Commit c47d65e61c7221307e680e5fa13184ac94c9450e

Authored by AntonioTerceiro
1 parent a1b3cac4

ActionItem154: converting svn:externals to piston



git-svn-id: https://svn.colivre.coop.br/svn/noosfero/trunk@1313 3f533792-8f58-4932-b0fe-aaf55b0a4547
Showing 154 changed files with 9315 additions and 0 deletions   Show diff stats
vendor/plugins/access_control/README 0 → 100644
@@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
  1 +AccessControl
  2 +=============
  3 +
  4 +Description goes here
0 \ No newline at end of file 5 \ No newline at end of file
vendor/plugins/access_control/Rakefile 0 → 100644
@@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
  1 +require 'rake'
  2 +require 'rake/testtask'
  3 +require 'rake/rdoctask'
  4 +
  5 +desc 'Default: run unit tests.'
  6 +task :default => :test
  7 +
  8 +desc 'Test the access_control plugin.'
  9 +Rake::TestTask.new(:test) do |t|
  10 + t.libs << 'lib'
  11 + t.pattern = 'test/**/*_test.rb'
  12 + t.verbose = true
  13 +end
  14 +
  15 +desc 'Generate documentation for the access_control plugin.'
  16 +Rake::RDocTask.new(:rdoc) do |rdoc|
  17 + rdoc.rdoc_dir = 'rdoc'
  18 + rdoc.title = 'AccessControl'
  19 + rdoc.options << '--line-numbers' << '--inline-source'
  20 + rdoc.rdoc_files.include('README')
  21 + rdoc.rdoc_files.include('lib/**/*.rb')
  22 +end
vendor/plugins/access_control/generators/access_control_migration/access_control_migration_generator.rb 0 → 100644
@@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
  1 +class AccessControlMigrationGenerator < Rails::Generator::Base
  2 + def manifest
  3 + record do |m|
  4 + m.migration_template 'migration.rb', 'db/migrate'
  5 + end
  6 + end
  7 +
  8 + def file_name
  9 + "access_control_migration"
  10 + end
  11 +end
  12 +
vendor/plugins/access_control/generators/access_control_migration/templates/migration.rb 0 → 100644
@@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
  1 +class AccessControlMigration < ActiveRecord::Migration
  2 + def self.up
  3 + create_table :roles do |t|
  4 + t.column :name, :string
  5 + t.column :permissions, :string
  6 + end
  7 +
  8 + create_table :role_assignments do |t|
  9 + t.column :accessor_id, :integer
  10 + t.column :accessor_type, :string
  11 + t.column :resource_id, :integer
  12 + t.column :resource_type, :string
  13 + t.column :role_id, :integer
  14 + t.column :is_global, :boolean
  15 + end
  16 + end
  17 +
  18 + def self.down
  19 + drop_table :roles
  20 + drop_table :role_assignments
  21 + end
  22 +end
vendor/plugins/access_control/init.rb 0 → 100644
@@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
  1 +require 'acts_as_accessor'
  2 +require 'acts_as_accessible'
  3 +require 'permission_name_helper'
  4 +module ApplicationHelper
  5 + include PermissionName
  6 +end
vendor/plugins/access_control/install.rb 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +# Install hook code here
vendor/plugins/access_control/lib/access_control.rb 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +# AccessControl
0 \ No newline at end of file 2 \ No newline at end of file
vendor/plugins/access_control/lib/acts_as_accessible.rb 0 → 100644
@@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
  1 +class ActiveRecord::Base
  2 + # This is the global hash of permissions and each item is of the form
  3 + # 'class_name' => permission_hash for each target have its own set of permissions
  4 + # but its not a namespace so each permission name should be unique
  5 + PERMISSIONS = {}
  6 +
  7 + # Acts as accessible makes a model acts as a resource that can be targeted by a permission
  8 + def self.acts_as_accessible
  9 + has_many :role_assignments, :as => :resource
  10 +
  11 + # A superior instance is an object that has higher level an thus can be targeted by a permission
  12 + # to represent an permission over a group of related resources rather than a single one
  13 + def superior_instance
  14 + nil
  15 + end
  16 +
  17 + def affiliate(accessor, roles)
  18 + roles = [roles] unless roles.kind_of?(Array)
  19 + roles.map {|role| accessor.add_role(role, self)}.any?
  20 + end
  21 +
  22 + def members
  23 + role_assignments.map(&:accessor).uniq
  24 + end
  25 + end
  26 +end
vendor/plugins/access_control/lib/acts_as_accessor.rb 0 → 100644
@@ -0,0 +1,58 @@ @@ -0,0 +1,58 @@
  1 +class ActiveRecord::Base
  2 + def self.acts_as_accessor
  3 + has_many :role_assignments, :as => :accessor
  4 +
  5 + def has_permission?(permission, resource = nil)
  6 + return true if resource == self
  7 + role_assignments.any? {|ra| ra.has_permission?(permission, resource)}
  8 + end
  9 +
  10 + def define_roles(roles, resource)
  11 + roles = [roles] unless roles.kind_of?(Array)
  12 + actual_roles = RoleAssignment.find( :all, :conditions => role_attributes(nil, resource) ).map(&:role)
  13 +
  14 + (roles - actual_roles).each {|r| add_role(r, resource) }
  15 + (actual_roles - roles).each {|r| remove_role(r, resource)}
  16 + end
  17 +
  18 + def add_role(role, resource)
  19 + attributes = role_attributes(role, resource)
  20 + if RoleAssignment.find(:all, :conditions => attributes).empty?
  21 + RoleAssignment.new(attributes).save
  22 + else
  23 + false
  24 + end
  25 + end
  26 +
  27 + def remove_role(role, resource)
  28 + return unless role
  29 + roles_destroy = RoleAssignment.find(:all, :conditions => role_attributes(role, resource))
  30 + return if roles_destroy.empty?
  31 + roles_destroy.map(&:destroy).all?
  32 + end
  33 +
  34 + def find_roles(res)
  35 + RoleAssignment.find(:all, :conditions => role_attributes(nil, res))
  36 + end
  37 +
  38 + protected
  39 + def role_attributes(role, resource)
  40 + attributes = {:accessor_id => self.id, :accessor_type => self.class.base_class.name}
  41 + if role
  42 + attributes[:role_id] = role.id
  43 + end
  44 + if resource == 'global'
  45 + attributes[:is_global] = true
  46 + resource = nil
  47 + end
  48 + if resource
  49 + attributes[:resource_id] = resource.id
  50 + attributes[:resource_type] = resource.class.base_class.name
  51 + else
  52 + attributes[:resource_id] = nil
  53 + attributes[:resource_type] = nil
  54 + end
  55 + attributes
  56 + end
  57 + end
  58 +end
vendor/plugins/access_control/lib/permission_check.rb 0 → 100644
@@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
  1 +module PermissionCheck
  2 +
  3 + module ClassMethods
  4 + # Declares the +permission+ need to be able to access +action+.
  5 + #
  6 + # * +permission+ must be a symbol or string naming the needed permission to
  7 + # access the specified actions.
  8 + # * +target+ is the object over witch the user would need the specified
  9 + # permission and must be specified as a symbol or the string 'global'. The controller using
  10 + # +target+ must respond to a method with that name returning the object
  11 + # against which the permissions needed will be checked or if 'global' is passed it will be
  12 + # cheked if the assignment is global
  13 + # * +accessor+ is a mehtod that returns the accessor who must have the permission. By default
  14 + # is :user
  15 + # * +action+ must be a hash of options for a before filter like
  16 + # :only => :index or :except => [:edit, :update] by default protects all the actions
  17 + def protect(permission, target_method, accessor_method = :user, actions = {})
  18 + actions, accessor_method = accessor_method, :user if accessor_method.kind_of?(Hash)
  19 + before_filter actions do |c|
  20 + target = target_method.kind_of?(Symbol) ? c.send(target_method) : target_method
  21 + accessor = accessor_method.kind_of?(Symbol) ? c.send(accessor_method) : accessor_method
  22 + unless accessor && accessor.has_permission?(permission.to_s, target)
  23 +# c.instance_variable_set('@b', [accessor, permission, target])
  24 + c.send(:render, :file => access_denied_template_path, :status => 403) && false
  25 + end
  26 + end
  27 + end
  28 +
  29 + def access_denied_template_path
  30 + if File.exists?(File.join(RAILS_ROOT, 'app', 'views','access_control' ,'access_denied.rhtml'))
  31 + file_path = File.join(RAILS_ROOT, 'app', 'views','access_control' ,'access_denied.rhtml')
  32 + else
  33 + file_path = File.join(File.dirname(__FILE__),'..', 'views','access_denied.rhtml')
  34 + end
  35 + end
  36 + end
  37 +
  38 + def self.included(including)
  39 + including.send(:extend, PermissionCheck::ClassMethods)
  40 + end
  41 +end
vendor/plugins/access_control/lib/permission_name_helper.rb 0 → 100644
@@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
  1 +module PermissionName
  2 + def permission_name(p)
  3 + msgid = ActiveRecord::Base::PERMISSIONS.values.inject({}){|s,v| s.merge(v)}[p]
  4 + gettext(msgid)
  5 + end
  6 +end
vendor/plugins/access_control/lib/role.rb 0 → 100644
@@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
  1 +class Role < ActiveRecord::Base
  2 +
  3 + has_many :role_assignments
  4 + serialize :permissions, Array
  5 + validates_presence_of :name
  6 + validates_uniqueness_of :name
  7 +
  8 + def initialize(*args)
  9 + super(*args)
  10 + self[:permissions] ||= []
  11 + end
  12 +
  13 + def has_permission?(perm)
  14 + permissions.include?(perm)
  15 + end
  16 +
  17 + def has_kind?(k)
  18 + permissions.any?{|p| perms[k].keys.include?(p)}
  19 + end
  20 +
  21 + def kind
  22 + perms.keys.detect{|k| perms[k].keys.include?(permissions[0]) }
  23 + end
  24 +
  25 + protected
  26 + def perms
  27 + ActiveRecord::Base::PERMISSIONS
  28 + end
  29 +end
vendor/plugins/access_control/lib/role_assignment.rb 0 → 100644
@@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
  1 +class RoleAssignment < ActiveRecord::Base
  2 + belongs_to :role
  3 + belongs_to :accessor, :polymorphic => true
  4 + belongs_to :resource, :polymorphic => true
  5 +
  6 + validates_presence_of :role, :accessor
  7 +
  8 + def has_permission?(perm, res)
  9 + return false unless role.has_permission?(perm.to_s) && (resource || is_global)
  10 + return true if is_global
  11 + return false if res == 'global'
  12 + while res
  13 + return true if (resource == res)
  14 + res = res.superior_instance
  15 + end
  16 + return (resource == res)
  17 + end
  18 +end
vendor/plugins/access_control/tasks/access_control_tasks.rake 0 → 100644
@@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
  1 +# desc "Explaining what the task does"
  2 +# task :access_control do
  3 +# # Task goes here
  4 +# end
0 \ No newline at end of file 5 \ No newline at end of file
vendor/plugins/access_control/test/access_control_test.rb 0 → 100644
@@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
  1 +require 'test/unit'
  2 +
  3 +class AccessControlTest < Test::Unit::TestCase
  4 +end
vendor/plugins/access_control/test/acts_as_accessible_test.rb 0 → 100644
@@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
  1 +require 'test/unit'
  2 +require File.dirname(__FILE__) + '/test_helper'
  3 +
  4 +class AccessControlTest < Test::Unit::TestCase
  5 + def test_can_have_role_in_respect_to_an_resource
  6 + r = AccessControlTestResource.create(:name => 'bla')
  7 + a = AccessControlTestAccessor.create(:name => 'ze')
  8 + member_role = Role.create(:name => 'member', :permissions => ['bli'])
  9 + r.affiliate(a, member_role)
  10 + assert a.has_permission?('bli', r)
  11 + end
  12 +end
vendor/plugins/access_control/test/acts_as_accessor_test.rb 0 → 100644
@@ -0,0 +1,58 @@ @@ -0,0 +1,58 @@
  1 +require 'test/unit'
  2 +require File.dirname(__FILE__) + '/test_helper'
  3 +
  4 +class ActAsAccessorTest < Test::Unit::TestCase
  5 + def test_can_have_role_in_respect_to_an_resource
  6 + res = AccessControlTestResource.create!(:name => 'bla')
  7 + a = AccessControlTestAccessor.create!(:name => 'ze')
  8 + role = Role.create!(:name => 'just_a_member', :permissions => ['bli'])
  9 + assert a.add_role(role, res)
  10 + assert a.has_permission?('bli', res)
  11 + end
  12 +
  13 + def test_can_have_a_global_role
  14 + r = AccessControlTestResource.create!(:name => 'bla')
  15 + a = AccessControlTestAccessor.create!(:name => 'ze')
  16 + member_role = Role.create!(:name => 'just_a_moderator', :permissions => ['bli'])
  17 + assert a.add_role(member_role, 'global')
  18 + assert a.has_permission?('bli', 'global')
  19 + end
  20 +
  21 + def test_add_role
  22 + res = AccessControlTestResource.create!(:name => 'bla')
  23 + a = AccessControlTestAccessor.create!(:name => 'ze')
  24 + role = Role.create!(:name => 'just_a_content_author', :permissions => ['bli'])
  25 + assert a.add_role(role, res)
  26 + assert a.role_assignments.map{|ra|[ra.role, ra.accessor, ra.resource]}.include?([role, a, res])
  27 + end
  28 +
  29 + def test_remove_role
  30 + res = AccessControlTestResource.create!(:name => 'bla')
  31 + a = AccessControlTestAccessor.create!(:name => 'ze')
  32 + role = Role.create!(:name => 'just_an_author', :permissions => ['bli'])
  33 + ra = RoleAssignment.create!(:accessor => a, :role => role, :resource => res)
  34 +
  35 + assert a.role_assignments.include?(ra)
  36 + assert a.remove_role(role, res)
  37 + a.reload
  38 + assert !a.role_assignments.map{|ra|[ra.role, ra.accessor, ra.resource]}.include?([role, a, res])
  39 + end
  40 +
  41 + def test_do_not_add_role_twice
  42 + res = AccessControlTestResource.create!(:name => 'bla')
  43 + a = AccessControlTestAccessor.create!(:name => 'ze')
  44 + role = Role.create!(:name => 'a_content_author', :permissions => ['bli'])
  45 + assert a.add_role(role, res)
  46 + assert !a.add_role(role, res)
  47 + assert a.role_assignments.map{|ra|[ra.role, ra.accessor, ra.resource]}.include?([role, a, res])
  48 + end
  49 +
  50 + def test_do_not_remove_inexistent_role
  51 + res = AccessControlTestResource.create!(:name => 'bla')
  52 + a = AccessControlTestAccessor.create!(:name => 'ze')
  53 + role = Role.create!(:name => 'an_author', :permissions => ['bli'])
  54 +
  55 + assert !a.role_assignments.map{|ra|[ra.role, ra.accessor, ra.resource]}.include?([role, a, res])
  56 + assert !a.remove_role(role, res)
  57 + end
  58 +end
vendor/plugins/access_control/test/permission_check_test.rb 0 → 100644
@@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
  1 +require File.join(File.dirname(__FILE__), 'test_helper')
  2 +
  3 +class AccessControlTestController; def rescue_action(e) raise e end; end
  4 +class PermissionCheckTest < Test::Unit::TestCase
  5 +
  6 + def setup
  7 + @controller = AccessControlTestController.new
  8 + @request = ActionController::TestRequest.new
  9 + @response = ActionController::TestResponse.new
  10 + end
  11 +
  12 + def test_access_denied
  13 + get :index
  14 + assert_response 403
  15 + assert_template 'access_denied.rhtml'
  16 + end
  17 +
  18 + def test_global_permission_granted
  19 + user = AccessControlTestAccessor.create!(:name => 'user')
  20 + role = Role.create!(:name => 'some_role', :permissions => ['see_index'])
  21 + assert user.add_role(role, 'global')
  22 + assert user.has_permission?('see_index', 'global')
  23 +
  24 + get :index, :user => user.id
  25 + assert_response :success
  26 + assert_template nil
  27 + end
  28 +
  29 + def test_specific_permission_granted
  30 + user = AccessControlTestAccessor.create!(:name => 'other_user')
  31 + role = Role.create!(:name => 'other_role', :permissions => ['do_some_stuff'])
  32 + resource = AccessControlTestResource.create!(:name => 'some_resource')
  33 + assert user.add_role(role, resource)
  34 + assert user.has_permission?('do_some_stuff', resource)
  35 +
  36 + get :other_stuff, :user => user.id, :resource => resource.id
  37 + assert_response :success
  38 + assert_template nil
  39 +
  40 + end
  41 +end
vendor/plugins/access_control/test/role_assignment_test.rb 0 → 100644
@@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
  1 +require File.dirname(__FILE__) + '/test_helper'
  2 +
  3 +class RoleAssignmentTest < Test::Unit::TestCase
  4 +
  5 + def test_has_global_permission
  6 + role = Role.create(:name => 'new_role', :permissions => ['permission'])
  7 + ra = RoleAssignment.create(:role => role, :is_global => true)
  8 + assert ra.has_permission?('permission', 'global')
  9 + assert !ra.has_permission?('not_permitted', 'global')
  10 + end
  11 +
  12 + def test_has_global_permission_with_global_resource
  13 + role = Role.create(:name => 'new_role', :permissions => ['permission'])
  14 + ra = RoleAssignment.create(:role => role, :is_global => true)
  15 + assert ra.has_permission?('permission', 'global')
  16 + assert !ra.has_permission?('not_permitted', 'global')
  17 + end
  18 +
  19 + def test_has_specific_permission
  20 + role = Role.create(:name => 'new_role', :permissions => ['permission'])
  21 + accessor = AccessControlTestAccessor.create(:name => 'accessor')
  22 + resource_A = AccessControlTestResource.create(:name => 'Resource A')
  23 + resource_B = AccessControlTestResource.create(:name => 'Resource B')
  24 + ra = RoleAssignment.create(:accessor => accessor, :role => role, :resource => resource_A)
  25 + assert !ra.new_record?
  26 + assert_equal role, ra.role
  27 + assert_equal accessor, ra.accessor
  28 + assert_equal resource_A, ra.resource
  29 + assert ra.has_permission?('permission', resource_A)
  30 + assert !ra.has_permission?('permission', resource_B)
  31 + end
  32 +end
vendor/plugins/access_control/test/role_test.rb 0 → 100644
@@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
  1 +require File.join(File.dirname(__FILE__), 'test_helper')
  2 +
  3 +
  4 +class RoleTest < Test::Unit::TestCase
  5 +
  6 + def test_role_creation
  7 + count = Role.count
  8 + role = Role.new(:name => 'any_role')
  9 + assert role.save
  10 + assert_equal count + 1, Role.count
  11 + end
  12 +
  13 + def test_uniqueness_of_name
  14 + Role.create(:name => 'role_name')
  15 + role = Role.new(:name => 'role_name')
  16 + assert ! role.save
  17 + end
  18 +
  19 + def test_permission_setting
  20 + role = Role.new(:name => 'permissive_role', :permissions => ['edit_profile'])
  21 + assert role.save
  22 + assert role.has_permission?('edit_profile')
  23 + role.permissions << 'post_content'
  24 + assert role.save
  25 + assert role.has_permission?('post_content')
  26 + assert role.has_permission?('edit_profile')
  27 + end
  28 +end
vendor/plugins/access_control/test/schema.rb 0 → 100644
@@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
  1 +ActiveRecord::Migration.verbose = false
  2 +
  3 +ActiveRecord::Schema.define(:version => 0) do
  4 +
  5 + create_table :access_control_test_roles, :force => true do |t|
  6 + t.column :name, :string
  7 + t.column :permissions, :string
  8 + end
  9 +
  10 + create_table :access_control_test_role_assignments, :force => true do |t|
  11 + t.column :role_id, :integer
  12 + t.column :accessor_id, :integer
  13 + t.column :accessor_type, :string
  14 + t.column :resource_id, :integer
  15 + t.column :resource_type, :string
  16 + t.column :is_global, :boolean
  17 + end
  18 +
  19 + create_table :access_control_test_accessors, :force => true do |t|
  20 + t.column :name, :string
  21 + end
  22 +
  23 + create_table :access_control_test_resources, :force => true do |t|
  24 + t.column :name, :string
  25 + end
  26 +end
  27 +
  28 +ActiveRecord::Migration.verbose = true
  29 +
  30 +
  31 +
vendor/plugins/access_control/test/test_helper.rb 0 → 100644
@@ -0,0 +1,51 @@ @@ -0,0 +1,51 @@
  1 +ENV["RAILS_ENV"] = "test"
  2 +require File.expand_path(File.dirname(__FILE__) + "/../../../../config/environment")
  3 +
  4 +require 'test/unit'
  5 +require 'mocha'
  6 +
  7 +# from Rails
  8 +require 'test_help'
  9 +
  10 +# load the database schema for the tests
  11 +ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
  12 +load(File.dirname(__FILE__) + '/schema.rb')
  13 +# change the table names for the tests to not touch
  14 +Role.set_table_name 'access_control_test_roles'
  15 +RoleAssignment.set_table_name 'access_control_test_role_assignments'
  16 +
  17 +# accessor example class to access some resources
  18 +class AccessControlTestAccessor < ActiveRecord::Base
  19 + set_table_name 'access_control_test_accessors'
  20 + acts_as_accessor
  21 +end
  22 +
  23 +# resource example class to be accessed by some accessor
  24 +class AccessControlTestResource < ActiveRecord::Base
  25 + set_table_name 'access_control_test_resources'
  26 + acts_as_accessible
  27 + PERMISSIONS[self.class.name] = {'bla' => N_('Bla')}
  28 +end
  29 +
  30 +# controller to test protection
  31 +class AccessControlTestController < ApplicationController
  32 + include PermissionCheck
  33 + protect 'see_index', 'global', :user, :only => :index
  34 + protect 'do_some_stuff', :resource, :user, :only => :other_stuff
  35 + def index
  36 + render :text => 'test controller'
  37 + end
  38 +
  39 + def other_stuff
  40 + render :text => 'test stuff'
  41 + end
  42 +
  43 +protected
  44 + def user
  45 + AccessControlTestAccessor.find(params[:user]) if params[:user]
  46 + end
  47 +
  48 + def resource
  49 + AccessControlTestResource.find(params[:resource]) if params[:resource]
  50 + end
  51 +end
vendor/plugins/access_control/uninstall.rb 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +# Uninstall hook code here
vendor/plugins/access_control/views/access_denied.rhtml 0 → 100644
@@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
  1 +<h2> <%= _('Access denied') %> </h2>
  2 +
  3 +<% unless @message.nil? %>
  4 + <p>
  5 + <%= @message %>
  6 + </p>
  7 +<% end %>
vendor/plugins/acts_as_ferret/LICENSE 0 → 100644
@@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
  1 +Copyright (c) 2006 Kasper Weibel, Jens Kraemer
  2 +
  3 +Permission is hereby granted, free of charge, to any person obtaining
  4 +a copy of this software and associated documentation files (the
  5 +"Software"), to deal in the Software without restriction, including
  6 +without limitation the rights to use, copy, modify, merge, publish,
  7 +distribute, sublicense, and/or sell copies of the Software, and to
  8 +permit persons to whom the Software is furnished to do so, subject to
  9 +the following conditions:
  10 +
  11 +The above copyright notice and this permission notice shall be
  12 +included in all copies or substantial portions of the Software.
  13 +
  14 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  15 +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  16 +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  17 +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  18 +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  19 +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  20 +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
vendor/plugins/acts_as_ferret/README 0 → 100644
@@ -0,0 +1,49 @@ @@ -0,0 +1,49 @@
  1 += acts_as_ferret
  2 +
  3 +This ActiveRecord mixin adds full text search capabilities to any Rails model.
  4 +
  5 +It is heavily based on the original acts_as_ferret plugin done by
  6 +Kasper Weibel and a modified version done by Thomas Lockney, which
  7 +both can be found on http://ferret.davebalmain.com/trac/wiki/FerretOnRails
  8 +
  9 +== Installation
  10 +
  11 +=== Installation inside your Rails project via script/plugin
  12 +
  13 +script/plugin install svn://projects.jkraemer.net/acts_as_ferret/trunk/plugin/acts_as_ferret
  14 +
  15 +
  16 +=== System-wide installation with Rubygems
  17 +
  18 +<tt>sudo gem install acts_as_ferret</tt>
  19 +
  20 +To use acts_as_ferret in your project, add the following line to your
  21 +project's config/environment.rb:
  22 +
  23 +<tt>require 'acts_as_ferret'</tt>
  24 +
  25 +
  26 +== Usage
  27 +
  28 +include the following in your model class (specifiying the fields you want to get indexed):
  29 +
  30 +<tt>acts_as_ferret :fields => [ :title, :description ]</tt>
  31 +
  32 +now you can use ModelClass.find_by_contents(query) to find instances of your model
  33 +whose indexed fields match a given query. All query terms are required by default,
  34 +but explicit OR queries are possible. This differs from the ferret default, but
  35 +imho is the more often needed/expected behaviour (more query terms result in
  36 +less results).
  37 +
  38 +Please see ActsAsFerret::ActMethods#acts_as_ferret for more information.
  39 +
  40 +== License
  41 +
  42 +Released under the MIT license.
  43 +
  44 +== Authors
  45 +
  46 +* Kasper Weibel Nielsen-Refs (original author)
  47 +* Jens Kraemer <jk@jkraemer.net> (current maintainer)
  48 +
  49 +
vendor/plugins/acts_as_ferret/config/ferret_server.yml 0 → 100644
@@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
  1 +production:
  2 + host: ferret.yourdomain.com
  3 + port: 9010
  4 + pid_file: log/ferret.pid
  5 +development:
  6 + host: localhost
  7 + port: 9010
  8 + pid_file: log/ferret.pid
  9 +test:
  10 + host: localhost
  11 + port: 9009
  12 + pid_file: log/ferret.pid
vendor/plugins/acts_as_ferret/init.rb 0 → 100644
@@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
  1 +# Copyright (c) 2006 Kasper Weibel Nielsen-Refs, Thomas Lockney, Jens Krämer
  2 +#
  3 +# Permission is hereby granted, free of charge, to any person obtaining a copy
  4 +# of this software and associated documentation files (the "Software"), to deal
  5 +# in the Software without restriction, including without limitation the rights
  6 +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  7 +# copies of the Software, and to permit persons to whom the Software is
  8 +# furnished to do so, subject to the following conditions:
  9 +#
  10 +# The above copyright notice and this permission notice shall be included in all
  11 +# copies or substantial portions of the Software.
  12 +#
  13 +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  14 +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  15 +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  16 +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  17 +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  18 +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  19 +# SOFTWARE.
  20 +
  21 +require 'acts_as_ferret'
  22 +
vendor/plugins/acts_as_ferret/install.rb 0 → 100644
@@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
  1 +# acts_as_ferret install script
  2 +require 'fileutils'
  3 +
  4 +def install(file)
  5 + puts "Installing: #{file}"
  6 + target = File.join(File.dirname(__FILE__), '..', '..', '..', file)
  7 + if File.exists?(target)
  8 + puts "target #{target} already exists, skipping"
  9 + else
  10 + FileUtils.cp File.join(File.dirname(__FILE__), file), target
  11 + end
  12 +end
  13 +
  14 +install File.join( 'script', 'ferret_start' )
  15 +install File.join( 'script', 'ferret_stop' )
  16 +install File.join( 'config', 'ferret_server.yml' )
  17 +
  18 +puts IO.read(File.join(File.dirname(__FILE__), 'README'))
  19 +
vendor/plugins/acts_as_ferret/lib/act_methods.rb 0 → 100644
@@ -0,0 +1,242 @@ @@ -0,0 +1,242 @@
  1 +module ActsAsFerret #:nodoc:
  2 +
  3 + # This module defines the acts_as_ferret method and is included into
  4 + # ActiveRecord::Base
  5 + module ActMethods
  6 +
  7 +
  8 + def reloadable?; false end
  9 +
  10 + # declares a class as ferret-searchable.
  11 + #
  12 + # ====options:
  13 + # fields:: names all fields to include in the index. If not given,
  14 + # all attributes of the class will be indexed. You may also give
  15 + # symbols pointing to instance methods of your model here, i.e.
  16 + # to retrieve and index data from a related model.
  17 + #
  18 + # additional_fields:: names fields to include in the index, in addition
  19 + # to those derived from the db scheme. use if you want
  20 + # to add custom fields derived from methods to the db
  21 + # fields (which will be picked by aaf). This option will
  22 + # be ignored when the fields option is given, in that
  23 + # case additional fields get specified there.
  24 + #
  25 + # index_dir:: declares the directory where to put the index for this class.
  26 + # The default is RAILS_ROOT/index/RAILS_ENV/CLASSNAME.
  27 + # The index directory will be created if it doesn't exist.
  28 + #
  29 + # single_index:: set this to true to let this class use a Ferret
  30 + # index that is shared by all classes having :single_index set to true.
  31 + # :store_class_name is set to true implicitly, as well as index_dir, so
  32 + # don't bother setting these when using this option. the shared index
  33 + # will be located in index/<RAILS_ENV>/shared .
  34 + #
  35 + # store_class_name:: to make search across multiple models (with either
  36 + # single_index or the multi_search method) useful, set
  37 + # this to true. the model class name will be stored in a keyword field
  38 + # named class_name
  39 + #
  40 + # reindex_batch_size:: reindexing is done in batches of this size, default is 1000
  41 + #
  42 + # ferret:: Hash of Options that directly influence the way the Ferret engine works. You
  43 + # can use most of the options the Ferret::I class accepts here, too. Among the
  44 + # more useful are:
  45 + #
  46 + # or_default:: whether query terms are required by
  47 + # default (the default, false), or not (true)
  48 + #
  49 + # analyzer:: the analyzer to use for query parsing (default: nil,
  50 + # which means the ferret StandardAnalyzer gets used)
  51 + #
  52 + # default_field:: use to set one or more fields that are searched for query terms
  53 + # that don't have an explicit field list. This list should *not*
  54 + # contain any untokenized fields. If it does, you're asking
  55 + # for trouble (i.e. not getting results for queries having
  56 + # stop words in them). Aaf by default initializes the default field
  57 + # list to contain all tokenized fields. If you use :single_index => true,
  58 + # you really should set this option specifying your default field
  59 + # list (which should be equal in all your classes sharing the index).
  60 + # Otherwise you might get incorrect search results and you won't get
  61 + # any lazy loading of stored field data.
  62 + #
  63 + # For downwards compatibility reasons you can also specify the Ferret options in the
  64 + # last Hash argument.
  65 + def acts_as_ferret(options={}, ferret_options={})
  66 +
  67 + # force local mode if running *inside* the Ferret server - somewhere the
  68 + # real indexing has to be done after all :-)
  69 + # Usually the automatic detection of server mode works fine, however if you
  70 + # require your model classes in environment.rb they will get loaded before the
  71 + # DRb server is started, so this code is executed too early and detection won't
  72 + # work. In this case you'll get endless loops resulting in "stack level too deep"
  73 + # errors.
  74 + # To get around this, start the server with the environment variable
  75 + # FERRET_USE_LOCAL_INDEX set to '1'.
  76 + logger.debug "Asked for a remote server ? #{options[:remote].inspect}, ENV[\"FERRET_USE_LOCAL_INDEX\"] is #{ENV["FERRET_USE_LOCAL_INDEX"].inspect}, looks like we are#{ActsAsFerret::Remote::Server.running || ENV['FERRET_USE_LOCAL_INDEX'] ? '' : ' not'} the server"
  77 + options.delete(:remote) if ENV["FERRET_USE_LOCAL_INDEX"] || ActsAsFerret::Remote::Server.running
  78 +
  79 + if options[:remote] && options[:remote] !~ /^druby/
  80 + # read server location from config/ferret_server.yml
  81 + options[:remote] = ActsAsFerret::Remote::Config.load("#{RAILS_ROOT}/config/ferret_server.yml")[:uri] rescue nil
  82 + end
  83 +
  84 + if options[:remote]
  85 + logger.debug "Will use remote index server which should be available at #{options[:remote]}"
  86 + else
  87 + logger.debug "Will use local index."
  88 + end
  89 +
  90 +
  91 + extend ClassMethods
  92 + extend SharedIndexClassMethods if options[:single_index]
  93 +
  94 + include InstanceMethods
  95 + include MoreLikeThis::InstanceMethods
  96 +
  97 + # AR hooks
  98 + after_create :ferret_create
  99 + after_update :ferret_update
  100 + after_destroy :ferret_destroy
  101 +
  102 + cattr_accessor :aaf_configuration
  103 +
  104 + # default config
  105 + self.aaf_configuration = {
  106 + :index_dir => "#{ActsAsFerret::index_dir}/#{self.name.underscore}",
  107 + :store_class_name => false,
  108 + :name => self.table_name,
  109 + :class_name => self.name,
  110 + :single_index => false,
  111 + :reindex_batch_size => 1000,
  112 + :ferret => {}, # Ferret config Hash
  113 + :ferret_fields => {} # list of indexed fields that will be filled later
  114 + }
  115 +
  116 + # merge aaf options with args
  117 + aaf_configuration.update(options) if options.is_a?(Hash)
  118 + # apply appropriate settings for shared index
  119 + if aaf_configuration[:single_index]
  120 + aaf_configuration[:index_dir] = "#{ActsAsFerret::index_dir}/shared"
  121 + aaf_configuration[:store_class_name] = true
  122 + end
  123 +
  124 + # set ferret default options
  125 + aaf_configuration[:ferret].reverse_merge!( :or_default => false,
  126 + :handle_parse_errors => true,
  127 + :default_field => nil # will be set later on
  128 + #:max_clauses => 512,
  129 + #:analyzer => Ferret::Analysis::StandardAnalyzer.new,
  130 + # :wild_card_downcase => true
  131 + )
  132 +
  133 + # merge ferret options with those from second parameter hash
  134 + aaf_configuration[:ferret].update(ferret_options) if ferret_options.is_a?(Hash)
  135 +
  136 + unless options[:remote]
  137 + ActsAsFerret::ensure_directory aaf_configuration[:index_dir]
  138 + aaf_configuration[:index_base_dir] = aaf_configuration[:index_dir]
  139 + aaf_configuration[:index_dir] = find_last_index_version(aaf_configuration[:index_dir])
  140 + logger.debug "using index in #{aaf_configuration[:index_dir]}"
  141 + end
  142 +
  143 + # these properties are somewhat vital to the plugin and shouldn't
  144 + # be overwritten by the user:
  145 + aaf_configuration[:ferret].update(
  146 + :key => (aaf_configuration[:single_index] ? [:id, :class_name] : :id),
  147 + :path => aaf_configuration[:index_dir],
  148 + :auto_flush => true, # slower but more secure in terms of locking problems TODO disable when running in drb mode?
  149 + :create_if_missing => true
  150 + )
  151 +
  152 + if aaf_configuration[:fields]
  153 + add_fields(aaf_configuration[:fields])
  154 + else
  155 + add_fields(self.new.attributes.keys.map { |k| k.to_sym })
  156 + add_fields(aaf_configuration[:additional_fields])
  157 + end
  158 +
  159 + # now that all fields have been added, we can initialize the default
  160 + # field list to be used by the query parser.
  161 + # It will include all content fields *not* marked as :untokenized.
  162 + # This fixes the otherwise failing CommentTest#test_stopwords. Basically
  163 + # this means that by default only tokenized fields (which is the default)
  164 + # will be searched. If you want to search inside the contents of an
  165 + # untokenized field, you'll have to explicitly specify it in your query.
  166 + #
  167 + # Unfortunately this is not very useful with a shared index (see
  168 + # http://projects.jkraemer.net/acts_as_ferret/ticket/85)
  169 + # You should consider specifying the default field list to search for as
  170 + # part of the ferret_options hash in your call to acts_as_ferret.
  171 + aaf_configuration[:ferret][:default_field] ||= if aaf_configuration[:single_index]
  172 + logger.warn "You really should set the acts_as_ferret :default_field option when using a shared index!"
  173 + '*'
  174 + else
  175 + aaf_configuration[:ferret_fields].keys.select do |f|
  176 + aaf_configuration[:ferret_fields][f][:index] != :untokenized
  177 + end
  178 + end
  179 + logger.info "default field list: #{aaf_configuration[:ferret][:default_field].inspect}"
  180 +
  181 + if options[:remote]
  182 + aaf_index.ensure_index_exists
  183 + end
  184 + end
  185 +
  186 +
  187 + protected
  188 +
  189 + # find the most recent version of an index
  190 + def find_last_index_version(basedir)
  191 + # check for versioned index
  192 + versions = Dir.entries(basedir).select do |f|
  193 + dir = File.join(basedir, f)
  194 + File.directory?(dir) && File.file?(File.join(dir, 'segments')) && f =~ /^\d+(_\d+)?$/
  195 + end
  196 + if versions.any?
  197 + # select latest version
  198 + versions.sort!
  199 + File.join basedir, versions.last
  200 + else
  201 + basedir
  202 + end
  203 + end
  204 +
  205 +
  206 + # helper that defines a method that adds the given field to a ferret
  207 + # document instance
  208 + def define_to_field_method(field, options = {})
  209 + options.reverse_merge!( :store => :no,
  210 + :highlight => :yes,
  211 + :index => :yes,
  212 + :term_vector => :with_positions_offsets,
  213 + :boost => 1.0 )
  214 + options[:term_vector] = :no if options[:index] == :no
  215 + aaf_configuration[:ferret_fields][field] = options
  216 + define_method("#{field}_to_ferret".to_sym) do
  217 + begin
  218 + val = content_for_field_name(field)
  219 + rescue
  220 + logger.warn("Error retrieving value for field #{field}: #{$!}")
  221 + val = ''
  222 + end
  223 + logger.debug("Adding field #{field} with value '#{val}' to index")
  224 + val
  225 + end
  226 + end
  227 +
  228 + def add_fields(field_config)
  229 + if field_config.is_a? Hash
  230 + field_config.each_pair do |key,val|
  231 + define_to_field_method(key,val)
  232 + end
  233 + elsif field_config.respond_to?(:each)
  234 + field_config.each do |field|
  235 + define_to_field_method(field)
  236 + end
  237 + end
  238 + end
  239 +
  240 + end
  241 +
  242 +end
vendor/plugins/acts_as_ferret/lib/acts_as_ferret.rb 0 → 100644
@@ -0,0 +1,160 @@ @@ -0,0 +1,160 @@
  1 +# Copyright (c) 2006 Kasper Weibel Nielsen-Refs, Thomas Lockney, Jens Krämer
  2 +#
  3 +# Permission is hereby granted, free of charge, to any person obtaining a copy
  4 +# of this software and associated documentation files (the "Software"), to deal
  5 +# in the Software without restriction, including without limitation the rights
  6 +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  7 +# copies of the Software, and to permit persons to whom the Software is
  8 +# furnished to do so, subject to the following conditions:
  9 +#
  10 +# The above copyright notice and this permission notice shall be included in all
  11 +# copies or substantial portions of the Software.
  12 +#
  13 +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  14 +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  15 +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  16 +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  17 +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  18 +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  19 +# SOFTWARE.
  20 +
  21 +require 'active_support'
  22 +require 'active_record'
  23 +require 'set'
  24 +require 'ferret'
  25 +
  26 +require 'ferret_extensions'
  27 +require 'act_methods'
  28 +require 'class_methods'
  29 +require 'shared_index_class_methods'
  30 +require 'ferret_result'
  31 +require 'instance_methods'
  32 +
  33 +require 'multi_index'
  34 +require 'more_like_this'
  35 +
  36 +require 'index'
  37 +require 'local_index'
  38 +require 'shared_index'
  39 +require 'remote_index'
  40 +
  41 +require 'ferret_server'
  42 +
  43 +
  44 +# The Rails ActiveRecord Ferret Mixin.
  45 +#
  46 +# This mixin adds full text search capabilities to any Rails model.
  47 +#
  48 +# The current version emerged from on the original acts_as_ferret plugin done by
  49 +# Kasper Weibel and a modified version done by Thomas Lockney, which both can be
  50 +# found on the Ferret Wiki: http://ferret.davebalmain.com/trac/wiki/FerretOnRails.
  51 +#
  52 +# basic usage:
  53 +# include the following in your model class (specifiying the fields you want to get indexed):
  54 +# acts_as_ferret :fields => [ :title, :description ]
  55 +#
  56 +# now you can use ModelClass.find_by_contents(query) to find instances of your model
  57 +# whose indexed fields match a given query. All query terms are required by default, but
  58 +# explicit OR queries are possible. This differs from the ferret default, but imho is the more
  59 +# often needed/expected behaviour (more query terms result in less results).
  60 +#
  61 +# Released under the MIT license.
  62 +#
  63 +# Authors:
  64 +# Kasper Weibel Nielsen-Refs (original author)
  65 +# Jens Kraemer <jk@jkraemer.net> (active maintainer)
  66 +#
  67 +module ActsAsFerret
  68 +
  69 + # global Hash containing all multi indexes created by all classes using the plugin
  70 + # key is the concatenation of alphabetically sorted names of the classes the
  71 + # searcher searches.
  72 + @@multi_indexes = Hash.new
  73 + def self.multi_indexes; @@multi_indexes end
  74 +
  75 + # global Hash containing the ferret indexes of all classes using the plugin
  76 + # key is the index directory.
  77 + @@ferret_indexes = Hash.new
  78 + def self.ferret_indexes; @@ferret_indexes end
  79 +
  80 +
  81 + # decorator that adds a total_hits accessor to search result arrays
  82 + class SearchResults
  83 + attr_reader :total_hits
  84 + def initialize(results, total_hits)
  85 + @results = results
  86 + @total_hits = total_hits
  87 + end
  88 + def method_missing(symbol, *args, &block)
  89 + @results.send(symbol, *args, &block)
  90 + end
  91 + def respond_to?(name)
  92 + self.methods.include?(name) || @results.respond_to?(name)
  93 + end
  94 + end
  95 +
  96 + def self.ensure_directory(dir)
  97 + FileUtils.mkdir_p dir unless (File.directory?(dir) || File.symlink?(dir))
  98 + end
  99 +
  100 + # make sure the default index base dir exists. by default, all indexes are created
  101 + # under RAILS_ROOT/index/RAILS_ENV
  102 + def self.init_index_basedir
  103 + index_base = "#{RAILS_ROOT}/index"
  104 + @@index_dir = "#{index_base}/#{RAILS_ENV}"
  105 + end
  106 +
  107 + mattr_accessor :index_dir
  108 + init_index_basedir
  109 +
  110 + def self.append_features(base)
  111 + super
  112 + base.extend(ClassMethods)
  113 + end
  114 +
  115 + # builds a FieldInfos instance for creation of an index containing fields
  116 + # for the given model classes.
  117 + def self.field_infos(models)
  118 + # default attributes for fields
  119 + fi = Ferret::Index::FieldInfos.new(:store => :no,
  120 + :index => :yes,
  121 + :term_vector => :no,
  122 + :boost => 1.0)
  123 + # primary key
  124 + fi.add_field(:id, :store => :yes, :index => :untokenized)
  125 + fields = {}
  126 + have_class_name = false
  127 + models.each do |model|
  128 + fields.update(model.aaf_configuration[:ferret_fields])
  129 + # class_name
  130 + if !have_class_name && model.aaf_configuration[:store_class_name]
  131 + fi.add_field(:class_name, :store => :yes, :index => :untokenized)
  132 + have_class_name = true
  133 + end
  134 + end
  135 + fields.each_pair do |field, options|
  136 + fi.add_field(field, { :store => :no,
  137 + :index => :yes }.update(options))
  138 + end
  139 + return fi
  140 + end
  141 +
  142 + def self.close_multi_indexes
  143 + # close combined index readers, just in case
  144 + # this seems to fix a strange test failure that seems to relate to a
  145 + # multi_index looking at an old version of the content_base index.
  146 + multi_indexes.each_pair do |key, index|
  147 + # puts "#{key} -- #{self.name}"
  148 + # TODO only close those where necessary (watch inheritance, where
  149 + # self.name is base class of a class where key is made from)
  150 + index.close #if key =~ /#{self.name}/
  151 + end
  152 + multi_indexes.clear
  153 + end
  154 +
  155 +end
  156 +
  157 +# include acts_as_ferret method into ActiveRecord::Base
  158 +ActiveRecord::Base.extend ActsAsFerret::ActMethods
  159 +
  160 +
vendor/plugins/acts_as_ferret/lib/class_methods.rb 0 → 100644
@@ -0,0 +1,316 @@ @@ -0,0 +1,316 @@
  1 +module ActsAsFerret
  2 +
  3 + module ClassMethods
  4 +
  5 + # rebuild the index from all data stored for this model.
  6 + # This is called automatically when no index exists yet.
  7 + #
  8 + # When calling this method manually, you can give any additional
  9 + # model classes that should also go into this index as parameters.
  10 + # Useful when using the :single_index option.
  11 + # Note that attributes named the same in different models will share
  12 + # the same field options in the shared index.
  13 + def rebuild_index(*models)
  14 + models << self unless models.include?(self)
  15 + aaf_index.rebuild_index models.map(&:to_s)
  16 + index_dir = find_last_index_version(aaf_configuration[:index_base_dir]) unless aaf_configuration[:remote]
  17 + end
  18 +
  19 + # runs across all records yielding those to be indexed when the index is rebuilt
  20 + def records_for_rebuild(batch_size = 1000)
  21 + transaction do
  22 + if connection.class.name =~ /Mysql/ && primary_key == 'id'
  23 + logger.info "using mysql specific batched find :all"
  24 + offset = 0
  25 + while (rows = find :all, :conditions => ["id > ?", offset ], :limit => batch_size).any?
  26 + offset = rows.last.id
  27 + yield rows, offset
  28 + end
  29 + else
  30 + # sql server adapter won't batch correctly without defined ordering
  31 + order = "#{primary_key} ASC" if connection.class.name =~ /SQLServer/
  32 + 0.step(self.count, batch_size) do |offset|
  33 + yield find( :all, :limit => batch_size, :offset => offset, :order => order ), offset
  34 + end
  35 + end
  36 + end
  37 + end
  38 +
  39 + # Switches this class to a new index located in dir.
  40 + # Used by the DRb server when switching to a new index version.
  41 + def index_dir=(dir)
  42 + logger.debug "changing index dir to #{dir}"
  43 + aaf_configuration[:index_dir] = aaf_configuration[:ferret][:path] = dir
  44 + aaf_index.reopen!
  45 + logger.debug "index dir is now #{dir}"
  46 + end
  47 +
  48 + # Retrieve the index instance for this model class. This can either be a
  49 + # LocalIndex, or a RemoteIndex instance.
  50 + #
  51 + # Index instances are stored in a hash, using the index directory
  52 + # as the key. So model classes sharing a single index will share their
  53 + # Index object, too.
  54 + def aaf_index
  55 + ActsAsFerret::ferret_indexes[aaf_configuration[:index_dir]] ||= create_index_instance
  56 + end
  57 +
  58 + # Finds instances by searching the Ferret index. Terms are ANDed by default, use
  59 + # OR between terms for ORed queries. Or specify +:or_default => true+ in the
  60 + # +:ferret+ options hash of acts_as_ferret.
  61 + #
  62 + # == options:
  63 + # offset:: first hit to retrieve (useful for paging)
  64 + # limit:: number of hits to retrieve, or :all to retrieve
  65 + # all results
  66 + # lazy:: Array of field names whose contents should be read directly
  67 + # from the index. Those fields have to be marked
  68 + # +:store => :yes+ in their field options. Give true to get all
  69 + # stored fields. Note that if you have a shared index, you have
  70 + # to explicitly state the fields you want to fetch, true won't
  71 + # work here)
  72 + # models:: only for single_index scenarios: an Array of other Model classes to
  73 + # include in this search. Use :all to query all models.
  74 + #
  75 + # +find_options+ is a hash passed on to active_record's find when
  76 + # retrieving the data from db, useful to i.e. prefetch relationships with
  77 + # :include or to specify additional filter criteria with :conditions.
  78 + #
  79 + # This method returns a +SearchResults+ instance, which really is an Array that has
  80 + # been decorated with a total_hits attribute holding the total number of hits.
  81 + #
  82 + # Please keep in mind that the number of total hits might be wrong if you specify
  83 + # both ferret options and active record find_options that somehow limit the result
  84 + # set (e.g. +:num_docs+ and some +:conditions+).
  85 + def find_with_ferret(q, options = {}, find_options = {})
  86 + total_hits, result = find_records_lazy_or_not q, options, find_options
  87 + logger.debug "Query: #{q}\ntotal hits: #{total_hits}, results delivered: #{result.size}"
  88 + return SearchResults.new(result, total_hits)
  89 + end
  90 + alias find_by_contents find_with_ferret
  91 +
  92 +
  93 +
  94 + # Returns the total number of hits for the given query
  95 + # To count the results of a multi_search query, specify an array of
  96 + # class names with the :models option.
  97 + def total_hits(q, options={})
  98 + if models = options[:models]
  99 + options[:models] = add_self_to_model_list_if_necessary(models).map(&:to_s)
  100 + end
  101 + aaf_index.total_hits(q, options)
  102 + end
  103 +
  104 + # Finds instance model name, ids and scores by contents.
  105 + # Useful e.g. if you want to search across models or do not want to fetch
  106 + # all result records (yet).
  107 + #
  108 + # Options are the same as for find_by_contents
  109 + #
  110 + # A block can be given too, it will be executed with every result:
  111 + # find_id_by_contents(q, options) do |model, id, score|
  112 + # id_array << id
  113 + # scores_by_id[id] = score
  114 + # end
  115 + # NOTE: in case a block is given, only the total_hits value will be returned
  116 + # instead of the [total_hits, results] array!
  117 + #
  118 + def find_id_by_contents(q, options = {}, &block)
  119 + deprecated_options_support(options)
  120 + aaf_index.find_id_by_contents(q, options, &block)
  121 + end
  122 +
  123 + # requires the store_class_name option of acts_as_ferret to be true
  124 + # for all models queried this way.
  125 + def multi_search(query, additional_models = [], options = {}, find_options = {})
  126 + result = []
  127 +
  128 + if options[:lazy]
  129 + logger.warn "find_options #{find_options} are ignored because :lazy => true" unless find_options.empty?
  130 + total_hits = id_multi_search(query, additional_models, options) do |model, id, score, data|
  131 + result << FerretResult.new(model, id, score, data)
  132 + end
  133 + else
  134 + id_arrays = {}
  135 + rank = 0
  136 + total_hits = id_multi_search(query, additional_models, options) do |model, id, score, data|
  137 + id_arrays[model] ||= {}
  138 + id_arrays[model][id] = [ rank += 1, score ]
  139 + end
  140 + result = retrieve_records(id_arrays, find_options)
  141 + end
  142 +
  143 + SearchResults.new(result, total_hits)
  144 + end
  145 +
  146 + # returns an array of hashes, each containing :class_name,
  147 + # :id and :score for a hit.
  148 + #
  149 + # if a block is given, class_name, id and score of each hit will
  150 + # be yielded, and the total number of hits is returned.
  151 + def id_multi_search(query, additional_models = [], options = {}, &proc)
  152 + deprecated_options_support(options)
  153 + additional_models = add_self_to_model_list_if_necessary(additional_models)
  154 + aaf_index.id_multi_search(query, additional_models.map(&:to_s), options, &proc)
  155 + end
  156 +
  157 +
  158 + protected
  159 +
  160 + def add_self_to_model_list_if_necessary(models)
  161 + models = [ models ] unless models.is_a? Array
  162 + models << self unless models.include?(self)
  163 + end
  164 +
  165 + def find_records_lazy_or_not(q, options = {}, find_options = {})
  166 + if options[:lazy]
  167 + logger.warn "find_options #{find_options} are ignored because :lazy => true" unless find_options.empty?
  168 + lazy_find_by_contents q, options
  169 + else
  170 + ar_find_by_contents q, options, find_options
  171 + end
  172 + end
  173 +
  174 + def ar_find_by_contents(q, options = {}, find_options = {})
  175 + result_ids = {}
  176 + total_hits = find_id_by_contents(q, options) do |model, id, score, data|
  177 + # stores ids, index of each id for later ordering of
  178 + # results, and score
  179 + result_ids[id] = [ result_ids.size + 1, score ]
  180 + end
  181 +
  182 + result = retrieve_records( { self.name => result_ids }, find_options )
  183 +
  184 + if find_options[:conditions]
  185 + if options[:limit] != :all
  186 + # correct result size if the user specified conditions
  187 + # wenn conditions: options[:limit] != :all --> ferret-query mit :all wiederholen und select count machen
  188 + result_ids = {}
  189 + find_id_by_contents(q, options.update(:limit => :all)) do |model, id, score, data|
  190 + result_ids[id] = [ result_ids.size + 1, score ]
  191 + end
  192 + total_hits = count_records( { self.name => result_ids }, find_options )
  193 + else
  194 + total_hits = result.length
  195 + end
  196 + end
  197 +
  198 + [ total_hits, result ]
  199 + end
  200 +
  201 + def lazy_find_by_contents(q, options = {})
  202 + result = []
  203 + total_hits = find_id_by_contents(q, options) do |model, id, score, data|
  204 + result << FerretResult.new(model, id, score, data)
  205 + end
  206 + [ total_hits, result ]
  207 + end
  208 +
  209 +
  210 + def model_find(model, id, find_options = {})
  211 + model.constantize.find(id, find_options)
  212 + end
  213 +
  214 + # retrieves search result records from a data structure like this:
  215 + # { 'Model1' => { '1' => [ rank, score ], '2' => [ rank, score ] }
  216 + #
  217 + # TODO: in case of STI AR will filter out hits from other
  218 + # classes for us, but this
  219 + # will lead to less results retrieved --> scoping of ferret query
  220 + # to self.class is still needed.
  221 + # from the ferret ML (thanks Curtis Hatter)
  222 + # > I created a method in my base STI class so I can scope my query. For scoping
  223 + # > I used something like the following line:
  224 + # >
  225 + # > query << " role:#{self.class.eql?(Contents) '*' : self.class}"
  226 + # >
  227 + # > Though you could make it more generic by simply asking
  228 + # > "self.descends_from_active_record?" which is how rails decides if it should
  229 + # > scope your "find" query for STI models. You can check out "base.rb" in
  230 + # > activerecord to see that.
  231 + # but maybe better do the scoping in find_id_by_contents...
  232 + def retrieve_records(id_arrays, find_options = {})
  233 + result = []
  234 + # get objects for each model
  235 + id_arrays.each do |model, id_array|
  236 + next if id_array.empty?
  237 + begin
  238 + model = model.constantize
  239 + rescue
  240 + raise "Please use ':store_class_name => true' if you want to use multi_search.\n#{$!}"
  241 + end
  242 +
  243 + # check for include association that might only exist on some models in case of multi_search
  244 + filtered_include_options = []
  245 + if include_options = find_options[:include]
  246 + include_options.each do |include_option|
  247 + filtered_include_options << include_option if model.reflections.has_key?(include_option.is_a?(Hash) ? include_option.keys[0].to_sym : include_option.to_sym)
  248 + end
  249 + end
  250 + filtered_include_options=nil if filtered_include_options.empty?
  251 +
  252 + # fetch
  253 + tmp_result = nil
  254 + model.send(:with_scope, :find => find_options) do
  255 + tmp_result = model.find( :all, :conditions => [
  256 + "#{model.table_name}.#{model.primary_key} in (?)", id_array.keys ],
  257 + :include => filtered_include_options )
  258 + end
  259 +
  260 + # set scores and rank
  261 + tmp_result.each do |record|
  262 + record.ferret_rank, record.ferret_score = id_array[record.id.to_s]
  263 + end
  264 + # merge with result array
  265 + result.concat tmp_result
  266 + end
  267 +
  268 + # order results as they were found by ferret, unless an AR :order
  269 + # option was given
  270 + result.sort! { |a, b| a.ferret_rank <=> b.ferret_rank } unless find_options[:order]
  271 + return result
  272 + end
  273 +
  274 + def count_records(id_arrays, find_options = {})
  275 + count = 0
  276 + id_arrays.each do |model, id_array|
  277 + next if id_array.empty?
  278 + begin
  279 + model = model.constantize
  280 + model.send(:with_scope, :find => find_options) do
  281 + count += model.count(:conditions => [ "#{model.table_name}.#{model.primary_key} in (?)",
  282 + id_array.keys ])
  283 + end
  284 + rescue TypeError
  285 + raise "#{model} must use :store_class_name option if you want to use multi_search against it.\n#{$!}"
  286 + end
  287 + end
  288 + count
  289 + end
  290 +
  291 + def deprecated_options_support(options)
  292 + if options[:num_docs]
  293 + logger.warn ":num_docs is deprecated, use :limit instead!"
  294 + options[:limit] ||= options[:num_docs]
  295 + end
  296 + if options[:first_doc]
  297 + logger.warn ":first_doc is deprecated, use :offset instead!"
  298 + options[:offset] ||= options[:first_doc]
  299 + end
  300 + end
  301 +
  302 + # creates a new Index instance.
  303 + def create_index_instance
  304 + if aaf_configuration[:remote]
  305 + RemoteIndex
  306 + elsif aaf_configuration[:single_index]
  307 + SharedIndex
  308 + else
  309 + LocalIndex
  310 + end.new(aaf_configuration)
  311 + end
  312 +
  313 + end
  314 +
  315 +end
  316 +
vendor/plugins/acts_as_ferret/lib/ferret_cap_tasks.rb 0 → 100644
@@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
  1 +# Ferret DRb server Capistrano tasks
  2 +# Usage:
  3 +# Add require 'vendor/plugins/acts_as_ferret/lib/ferret_cap_tasks' to your
  4 +# config/deploy.rb
  5 +# call ferret.restart where you restart your Mongrels.
  6 +# ferret.stop and ferret.start are available, too.
  7 +module FerretCapTasks
  8 + def start
  9 + run "cd #{current_path}; RAILS_ENV=production script/ferret_start"
  10 + end
  11 +
  12 + def stop
  13 + run "cd #{current_path}; RAILS_ENV=production script/ferret_stop"
  14 + end
  15 +
  16 + def restart
  17 + stop
  18 + start
  19 + end
  20 +end
  21 +Capistrano.plugin :ferret, FerretCapTasks
vendor/plugins/acts_as_ferret/lib/ferret_extensions.rb 0 → 100644
@@ -0,0 +1,81 @@ @@ -0,0 +1,81 @@
  1 +module Ferret
  2 +
  3 +
  4 + class Index::Index
  5 + attr_accessor :batch_size
  6 + attr_accessor :logger
  7 +
  8 + def index_models(models)
  9 + models.each { |model| index_model model }
  10 + flush
  11 + optimize
  12 + close
  13 + ActsAsFerret::close_multi_indexes
  14 + end
  15 +
  16 + def index_model(model)
  17 + @batch_size ||= 0
  18 + work_done = 0
  19 + batch_time = 0
  20 + logger.info "reindexing model #{model.name}"
  21 +
  22 + model_count = model.count.to_f
  23 + model.records_for_rebuild(@batch_size) do |records, offset|
  24 + #records = [ records ] unless records.is_a?(Array)
  25 + batch_time = measure_time {
  26 + records.each { |rec| self << rec.to_doc if rec.ferret_enabled?(true) }
  27 + }.to_f
  28 + work_done = offset.to_f / model_count * 100.0 if model_count > 0
  29 + remaining_time = ( batch_time / @batch_size ) * ( model_count - offset + @batch_size )
  30 + logger.info "reindex model #{model.name} : #{'%.2f' % work_done}% complete : #{'%.2f' % remaining_time} secs to finish"
  31 + end
  32 + end
  33 +
  34 + def measure_time
  35 + t1 = Time.now
  36 + yield
  37 + Time.now - t1
  38 + end
  39 +
  40 + end
  41 +
  42 +
  43 + # add marshalling support to SortFields
  44 + class Search::SortField
  45 + def _dump(depth)
  46 + to_s
  47 + end
  48 +
  49 + def self._load(string)
  50 + case string
  51 + when /<DOC(_ID)?>!/ : Ferret::Search::SortField::DOC_ID_REV
  52 + when /<DOC(_ID)?>/ : Ferret::Search::SortField::DOC_ID
  53 + when '<SCORE>!' : Ferret::Search::SortField::SCORE_REV
  54 + when '<SCORE>' : Ferret::Search::SortField::SCORE
  55 + when /^(\w+):<(\w+)>(!)?$/ : new($1.to_sym, :type => $2.to_sym, :reverse => !$3.nil?)
  56 + else raise "invalid value: #{string}"
  57 + end
  58 + end
  59 + end
  60 +
  61 + # add marshalling support to Sort
  62 + class Search::Sort
  63 + def _dump(depth)
  64 + to_s
  65 + end
  66 +
  67 + def self._load(string)
  68 + # we exclude the last <DOC> sorting as it is appended by new anyway
  69 + if string =~ /^Sort\[(.*?)(<DOC>(!)?)?\]$/
  70 + sort_fields = $1.split(',').map do |value|
  71 + value.strip!
  72 + Ferret::Search::SortField._load value unless value.blank?
  73 + end
  74 + new sort_fields.compact
  75 + else
  76 + raise "invalid value: #{string}"
  77 + end
  78 + end
  79 + end
  80 +
  81 +end
vendor/plugins/acts_as_ferret/lib/ferret_result.rb 0 → 100644
@@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
  1 +module ActsAsFerret
  2 +
  3 + # mixed into the FerretResult and AR classes calling acts_as_ferret
  4 + module ResultAttributes
  5 + # holds the score this record had when it was found via
  6 + # acts_as_ferret
  7 + attr_accessor :ferret_score
  8 +
  9 + attr_accessor :ferret_rank
  10 + end
  11 +
  12 + class FerretResult
  13 + include ResultAttributes
  14 + attr_accessor :id
  15 +
  16 + def initialize(model, id, score, data = {})
  17 + @model = model.constantize
  18 + @id = id
  19 + @ferret_score = score
  20 + @data = data
  21 + end
  22 +
  23 + def method_missing(method, *args)
  24 + if @ar_record || @data[method].nil?
  25 + ferret_load_record unless @ar_record
  26 + @ar_record.send method, *args
  27 + else
  28 + @data[method]
  29 + end
  30 + end
  31 +
  32 + def ferret_load_record
  33 + @ar_record = @model.find(id)
  34 + end
  35 + end
  36 +end
vendor/plugins/acts_as_ferret/lib/ferret_server.rb 0 → 100644
@@ -0,0 +1,131 @@ @@ -0,0 +1,131 @@
  1 +require 'drb'
  2 +require 'thread'
  3 +require 'yaml'
  4 +require 'erb'
  5 +
  6 +
  7 +module ActsAsFerret
  8 +
  9 + module Remote
  10 +
  11 + module Config
  12 + class << self
  13 + DEFAULTS = {
  14 + 'host' => 'localhost',
  15 + 'port' => '9009'
  16 + }
  17 + # read connection settings from config file
  18 + def load(file = "#{RAILS_ROOT}/config/ferret_server.yml")
  19 + config = DEFAULTS.merge(YAML.load(ERB.new(IO.read(file)).result))
  20 + if config = config[RAILS_ENV]
  21 + config[:uri] = "druby://#{config['host']}:#{config['port']}"
  22 + return config
  23 + end
  24 + {}
  25 + end
  26 + end
  27 + end
  28 +
  29 + # This class acts as a drb server listening for indexing and
  30 + # search requests from models declared to 'acts_as_ferret :remote => true'
  31 + #
  32 + # Usage:
  33 + # - modify RAILS_ROOT/config/ferret_server.yml to suit your needs.
  34 + # - environments for which no section in the config file exists will use
  35 + # the index locally (good for unit tests/development mode)
  36 + # - run script/ferret_start to start the server:
  37 + # RAILS_ENV=production script/ferret_start
  38 + #
  39 + class Server
  40 +
  41 + cattr_accessor :running
  42 +
  43 + def self.start(uri = nil)
  44 + ActiveRecord::Base.allow_concurrency = true
  45 + ActiveRecord::Base.logger = Logger.new("#{RAILS_ROOT}/log/ferret_server.log")
  46 + uri ||= ActsAsFerret::Remote::Config.load[:uri]
  47 + DRb.start_service(uri, ActsAsFerret::Remote::Server.new)
  48 + self.running = true
  49 + end
  50 +
  51 + def initialize
  52 + @logger = ActiveRecord::Base.logger
  53 + end
  54 +
  55 + # handles all incoming method calls, and sends them on to the LocalIndex
  56 + # instance of the correct model class.
  57 + #
  58 + # Calls are not queued atm, so this will block until the call returned.
  59 + #
  60 + def method_missing(name, *args)
  61 + @logger.debug "\#method_missing(#{name.inspect}, #{args.inspect})"
  62 + with_class args.shift do |clazz|
  63 + begin
  64 + clazz.aaf_index.send name, *args
  65 + rescue NoMethodError
  66 + @logger.debug "no luck, trying to call class method instead"
  67 + clazz.send name, *args
  68 + end
  69 + end
  70 + rescue
  71 + @logger.error "ferret server error #{$!}\n#{$!.backtrace.join '\n'}"
  72 + raise
  73 + end
  74 +
  75 + # make sure we have a versioned index in place, building one if necessary
  76 + def ensure_index_exists(class_name)
  77 + @logger.debug "DRb server: ensure_index_exists for class #{class_name}"
  78 + with_class class_name do |clazz|
  79 + dir = clazz.aaf_configuration[:index_dir]
  80 + unless File.directory?(dir) && File.file?(File.join(dir, 'segments')) && dir =~ %r{/\d+(_\d+)?$}
  81 + rebuild_index(clazz)
  82 + end
  83 + end
  84 + end
  85 +
  86 + # hides LocalIndex#rebuild_index to implement index versioning
  87 + def rebuild_index(clazz, *models)
  88 + with_class clazz do |clazz|
  89 + models = models.flatten.uniq.map(&:constantize)
  90 + models << clazz unless models.include?(clazz)
  91 + index = new_index_for(clazz, models)
  92 + @logger.debug "DRb server: rebuild index for class(es) #{models.inspect} in #{index.options[:path]}"
  93 + index.index_models models
  94 + new_version = File.join clazz.aaf_configuration[:index_base_dir], Time.now.utc.strftime('%Y%m%d%H%M%S')
  95 + # create a unique directory name (needed for unit tests where
  96 + # multiple rebuilds per second may occur)
  97 + if File.exists?(new_version)
  98 + i = 0
  99 + i+=1 while File.exists?("#{new_version}_#{i}")
  100 + new_version << "_#{i}"
  101 + end
  102 +
  103 + File.rename index.options[:path], new_version
  104 + clazz.index_dir = new_version
  105 + end
  106 + end
  107 +
  108 +
  109 + protected
  110 +
  111 + def with_class(clazz, *args)
  112 + clazz = clazz.constantize if String === clazz
  113 + yield clazz, *args
  114 + end
  115 +
  116 + def new_index_for(clazz, models)
  117 + aaf_configuration = clazz.aaf_configuration
  118 + ferret_cfg = aaf_configuration[:ferret].dup
  119 + ferret_cfg.update :auto_flush => false,
  120 + :create => true,
  121 + :field_infos => ActsAsFerret::field_infos(models),
  122 + :path => File.join(aaf_configuration[:index_base_dir], 'rebuild')
  123 + returning Ferret::Index::Index.new ferret_cfg do |i|
  124 + i.batch_size = aaf_configuration[:reindex_batch_size]
  125 + i.logger = @logger
  126 + end
  127 + end
  128 +
  129 + end
  130 + end
  131 +end
vendor/plugins/acts_as_ferret/lib/index.rb 0 → 100644
@@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
  1 +module ActsAsFerret
  2 +
  3 + # base class for local and remote indexes
  4 + class AbstractIndex
  5 +
  6 + attr_reader :aaf_configuration
  7 + attr_accessor :logger
  8 + def initialize(aaf_configuration)
  9 + @aaf_configuration = aaf_configuration
  10 + @logger = Logger.new("#{RAILS_ROOT}/log/ferret_index.log")
  11 + end
  12 +
  13 + class << self
  14 + def proxy_method(name, *args)
  15 + define_method name do |*args|
  16 + @server.send name, model_class_name, *args
  17 + end
  18 + end
  19 +
  20 + def index_proxy_method(*names)
  21 + names.each do |name|
  22 + define_method name do |*args|
  23 + @server.send :"index_#{name}", model_class_name, *args
  24 + end
  25 + end
  26 + end
  27 +
  28 + end
  29 + end
  30 +
  31 +end
vendor/plugins/acts_as_ferret/lib/instance_methods.rb 0 → 100644
@@ -0,0 +1,126 @@ @@ -0,0 +1,126 @@
  1 +module ActsAsFerret #:nodoc:
  2 +
  3 + module InstanceMethods
  4 + include ResultAttributes
  5 +
  6 + # Returns an array of strings with the matches highlighted. The +query+ can
  7 + # either be a String or a Ferret::Search::Query object.
  8 + #
  9 + # === Options
  10 + #
  11 + # field:: field to take the content from. This field has
  12 + # to have it's content stored in the index
  13 + # (:store => :yes in your call to aaf). If not
  14 + # given, all stored fields are searched, and the
  15 + # highlighted content found in all of them is returned.
  16 + # set :highlight => :no in the field options to
  17 + # avoid highlighting of contents from a :stored field.
  18 + # excerpt_length:: Default: 150. Length of excerpt to show. Highlighted
  19 + # terms will be in the centre of the excerpt.
  20 + # num_excerpts:: Default: 2. Number of excerpts to return.
  21 + # pre_tag:: Default: "<em>". Tag to place to the left of the
  22 + # match.
  23 + # post_tag:: Default: "</em>". This tag should close the
  24 + # +:pre_tag+.
  25 + # ellipsis:: Default: "...". This is the string that is appended
  26 + # at the beginning and end of excerpts (unless the
  27 + # excerpt hits the start or end of the field. You'll
  28 + # probably want to change this so a Unicode elipsis
  29 + # character.
  30 + def highlight(query, options = {})
  31 + self.class.aaf_index.highlight(id, self.class.name, query, options)
  32 + end
  33 +
  34 + # re-eneable ferret indexing after a call to #disable_ferret
  35 + def ferret_enable; @ferret_disabled = nil end
  36 +
  37 + # returns true if ferret indexing is enabled
  38 + # the optional parameter will be true if the method is called by rebuild_index,
  39 + # and false otherwise. I.e. useful to enable a model only for indexing during
  40 + # scheduled reindex runs.
  41 + def ferret_enabled?(is_rebuild = false); @ferret_disabled.nil? end
  42 +
  43 + # Disable Ferret for a specified amount of time. ::once will disable
  44 + # Ferret for the next call to #save (this is the default), ::always will
  45 + # do so for all subsequent calls.
  46 + # To manually trigger reindexing of a record, you can call #ferret_update
  47 + # directly.
  48 + #
  49 + # When given a block, this will be executed without any ferret indexing of
  50 + # this object taking place. The optional argument in this case can be used
  51 + # to indicate if the object should be indexed after executing the block
  52 + # (::index_when_finished). Automatic Ferret indexing of this object will be
  53 + # turned on after the block has been executed. If passed ::index_when_true,
  54 + # the index will only be updated if the block evaluated not to false or nil.
  55 + def disable_ferret(option = :once)
  56 + if block_given?
  57 + @ferret_disabled = :always
  58 + result = yield
  59 + ferret_enable
  60 + ferret_update if option == :index_when_finished || (option == :index_when_true && result)
  61 + result
  62 + elsif [:once, :always].include?(option)
  63 + @ferret_disabled = option
  64 + else
  65 + raise ArgumentError.new("Invalid Argument #{option}")
  66 + end
  67 + end
  68 +
  69 + # add to index
  70 + def ferret_create
  71 + if ferret_enabled?
  72 + logger.debug "ferret_create/update: #{self.class.name} : #{self.id}"
  73 + self.class.aaf_index << self
  74 + else
  75 + ferret_enable if @ferret_disabled == :once
  76 + end
  77 + true # signal success to AR
  78 + end
  79 + alias :ferret_update :ferret_create
  80 +
  81 +
  82 + # remove from index
  83 + def ferret_destroy
  84 + logger.debug "ferret_destroy: #{self.class.name} : #{self.id}"
  85 + begin
  86 + self.class.aaf_index.remove self.id, self.class.name
  87 + rescue
  88 + logger.warn("Could not find indexed value for this object: #{$!}\n#{$!.backtrace}")
  89 + end
  90 + true # signal success to AR
  91 + end
  92 +
  93 + # turn this instance into a ferret document (which basically is a hash of
  94 + # fieldname => value pairs)
  95 + def to_doc
  96 + logger.debug "creating doc for class: #{self.class.name}, id: #{self.id}"
  97 + returning doc = Ferret::Document.new do
  98 + # store the id of each item
  99 + doc[:id] = self.id
  100 +
  101 + # store the class name if configured to do so
  102 + doc[:class_name] = self.class.name if aaf_configuration[:store_class_name]
  103 +
  104 + # iterate through the fields and add them to the document
  105 + aaf_configuration[:ferret_fields].each_pair do |field, config|
  106 + doc[field] = self.send("#{field}_to_ferret") unless config[:ignore]
  107 + end
  108 + end
  109 + end
  110 +
  111 + def document_number
  112 + self.class.aaf_index.document_number(id, self.class.name)
  113 + end
  114 +
  115 + def query_for_record
  116 + self.class.aaf_index.query_for_record(id, self.class.name)
  117 + end
  118 +
  119 + def content_for_field_name(field)
  120 + self[field] || self.instance_variable_get("@#{field.to_s}".to_sym) || self.send(field.to_sym)
  121 + end
  122 +
  123 +
  124 + end
  125 +
  126 +end
vendor/plugins/acts_as_ferret/lib/local_index.rb 0 → 100644
@@ -0,0 +1,209 @@ @@ -0,0 +1,209 @@
  1 +module ActsAsFerret
  2 +
  3 + class LocalIndex < AbstractIndex
  4 + include MoreLikeThis::IndexMethods
  5 +
  6 +
  7 + def initialize(aaf_configuration)
  8 + super
  9 + ensure_index_exists
  10 + end
  11 +
  12 + def reopen!
  13 + if @ferret_index
  14 + @ferret_index.close
  15 + @ferret_index = nil
  16 + end
  17 + logger.debug "reopening index at #{aaf_configuration[:ferret][:path]}"
  18 + ferret_index
  19 + end
  20 +
  21 + # The 'real' Ferret Index instance
  22 + def ferret_index
  23 + ensure_index_exists
  24 + returning @ferret_index ||= Ferret::Index::Index.new(aaf_configuration[:ferret]) do
  25 + @ferret_index.batch_size = aaf_configuration[:reindex_batch_size]
  26 + @ferret_index.logger = logger
  27 + end
  28 + end
  29 +
  30 + # Checks for the presence of a segments file in the index directory
  31 + # Rebuilds the index if none exists.
  32 + def ensure_index_exists
  33 + logger.debug "LocalIndex: ensure_index_exists at #{aaf_configuration[:index_dir]}"
  34 + unless File.file? "#{aaf_configuration[:index_dir]}/segments"
  35 + ActsAsFerret::ensure_directory(aaf_configuration[:index_dir])
  36 + close
  37 + rebuild_index
  38 + end
  39 + end
  40 +
  41 + # Closes the underlying index instance
  42 + def close
  43 + @ferret_index.close if @ferret_index
  44 + rescue StandardError
  45 + # is raised when index already closed
  46 + ensure
  47 + @ferret_index = nil
  48 + end
  49 +
  50 + # rebuilds the index from all records of the model class this index belongs
  51 + # to. Arguments can be given in shared index scenarios to name multiple
  52 + # model classes to include in the index
  53 + def rebuild_index(*models)
  54 + models << aaf_configuration[:class_name] unless models.include?(aaf_configuration[:class_name])
  55 + models = models.flatten.uniq.map(&:constantize)
  56 + logger.debug "rebuild index: #{models.inspect}"
  57 + index = Ferret::Index::Index.new(aaf_configuration[:ferret].dup.update(:auto_flush => false,
  58 + :field_infos => ActsAsFerret::field_infos(models),
  59 + :create => true))
  60 + index.batch_size = aaf_configuration[:reindex_batch_size]
  61 + index.logger = logger
  62 + index.index_models models
  63 + end
  64 +
  65 + # Parses the given query string into a Ferret Query object.
  66 + def process_query(query)
  67 + # work around ferret bug in #process_query (doesn't ensure the
  68 + # reader is open)
  69 + ferret_index.synchronize do
  70 + ferret_index.send(:ensure_reader_open)
  71 + original_query = ferret_index.process_query(query)
  72 + end
  73 + end
  74 +
  75 + # Total number of hits for the given query.
  76 + # To count the results of a multi_search query, specify an array of
  77 + # class names with the :models option.
  78 + def total_hits(query, options = {})
  79 + index = (models = options.delete(:models)) ? multi_index(models) : ferret_index
  80 + index.search(query, options).total_hits
  81 + end
  82 +
  83 + def determine_lazy_fields(options = {})
  84 + stored_fields = options[:lazy]
  85 + if stored_fields && !(Array === stored_fields)
  86 + stored_fields = aaf_configuration[:ferret_fields].select { |field, config| config[:store] == :yes }.map(&:first)
  87 + end
  88 + logger.debug "stored_fields: #{stored_fields}"
  89 + return stored_fields
  90 + end
  91 +
  92 + # Queries the Ferret index to retrieve model class, id, score and the
  93 + # values of any fields stored in the index for each hit.
  94 + # If a block is given, these are yielded and the number of total hits is
  95 + # returned. Otherwise [total_hits, result_array] is returned.
  96 + def find_id_by_contents(query, options = {})
  97 + result = []
  98 + index = ferret_index
  99 + logger.debug "query: #{ferret_index.process_query query}" # TODO only enable this for debugging purposes
  100 + lazy_fields = determine_lazy_fields options
  101 +
  102 + total_hits = index.search_each(query, options) do |hit, score|
  103 + doc = index[hit]
  104 + model = aaf_configuration[:store_class_name] ? doc[:class_name] : aaf_configuration[:class_name]
  105 + # fetch stored fields if lazy loading
  106 + data = {}
  107 + lazy_fields.each { |field| data[field] = doc[field] } if lazy_fields
  108 + if block_given?
  109 + yield model, doc[:id], score, data
  110 + else
  111 + result << { :model => model, :id => doc[:id], :score => score, :data => data }
  112 + end
  113 + end
  114 + #logger.debug "id_score_model array: #{result.inspect}"
  115 + return block_given? ? total_hits : [total_hits, result]
  116 + end
  117 +
  118 + # Queries multiple Ferret indexes to retrieve model class, id and score for
  119 + # each hit. Use the models parameter to give the list of models to search.
  120 + # If a block is given, model, id and score are yielded and the number of
  121 + # total hits is returned. Otherwise [total_hits, result_array] is returned.
  122 + def id_multi_search(query, models, options = {})
  123 + index = multi_index(models)
  124 + result = []
  125 + lazy_fields = determine_lazy_fields options
  126 + total_hits = index.search_each(query, options) do |hit, score|
  127 + doc = index[hit]
  128 + # fetch stored fields if lazy loading
  129 + data = {}
  130 + lazy_fields.each { |field| data[field] = doc[field] } if lazy_fields
  131 + raise "':store_class_name => true' required for multi_search to work" if doc[:class_name].blank?
  132 + if block_given?
  133 + yield doc[:class_name], doc[:id], score, doc, data
  134 + else
  135 + result << { :model => doc[:class_name], :id => doc[:id], :score => score, :data => data }
  136 + end
  137 + end
  138 + return block_given? ? total_hits : [ total_hits, result ]
  139 + end
  140 +
  141 + ######################################
  142 + # methods working on a single record
  143 + # called from instance_methods, here to simplify interfacing with the
  144 + # remote ferret server
  145 + # TODO having to pass id and class_name around like this isn't nice
  146 + ######################################
  147 +
  148 + # add record to index
  149 + # record may be the full AR object, a Ferret document instance or a Hash
  150 + def add(record)
  151 + record = record.to_doc unless Hash === record || Ferret::Document === record
  152 + ferret_index << record
  153 + end
  154 + alias << add
  155 +
  156 + # delete record from index
  157 + def remove(id, class_name)
  158 + ferret_index.query_delete query_for_record(id, class_name)
  159 + end
  160 +
  161 + # highlight search terms for the record with the given id.
  162 + def highlight(id, class_name, query, options = {})
  163 + options.reverse_merge! :num_excerpts => 2, :pre_tag => '<em>', :post_tag => '</em>'
  164 + highlights = []
  165 + ferret_index.synchronize do
  166 + doc_num = document_number(id, class_name)
  167 + if options[:field]
  168 + highlights << ferret_index.highlight(query, doc_num, options)
  169 + else
  170 + query = process_query(query) # process only once
  171 + aaf_configuration[:ferret_fields].each_pair do |field, config|
  172 + next if config[:store] == :no || config[:highlight] == :no
  173 + options[:field] = field
  174 + highlights << ferret_index.highlight(query, doc_num, options)
  175 + end
  176 + end
  177 + end
  178 + return highlights.compact.flatten[0..options[:num_excerpts]-1]
  179 + end
  180 +
  181 + # retrieves the ferret document number of the record with the given id.
  182 + def document_number(id, class_name)
  183 + hits = ferret_index.search(query_for_record(id, class_name))
  184 + return hits.hits.first.doc if hits.total_hits == 1
  185 + raise "cannot determine document number from primary key: #{id}"
  186 + end
  187 +
  188 + # build a ferret query matching only the record with the given id
  189 + # the class name only needs to be given in case of a shared index configuration
  190 + def query_for_record(id, class_name = nil)
  191 + Ferret::Search::TermQuery.new(:id, id.to_s)
  192 + end
  193 +
  194 +
  195 + protected
  196 +
  197 + # returns a MultiIndex instance operating on a MultiReader
  198 + def multi_index(model_classes)
  199 + model_classes.map!(&:constantize) if String === model_classes.first
  200 + model_classes.sort! { |a, b| a.name <=> b.name }
  201 + key = model_classes.inject("") { |s, clazz| s + clazz.name }
  202 + multi_config = aaf_configuration[:ferret].dup
  203 + multi_config.delete :default_field # we don't want the default field list of *this* class for multi_searching
  204 + ActsAsFerret::multi_indexes[key] ||= MultiIndex.new(model_classes, multi_config)
  205 + end
  206 +
  207 + end
  208 +
  209 +end
vendor/plugins/acts_as_ferret/lib/more_like_this.rb 0 → 100644
@@ -0,0 +1,217 @@ @@ -0,0 +1,217 @@
  1 +module ActsAsFerret #:nodoc:
  2 +
  3 + module MoreLikeThis
  4 +
  5 + module InstanceMethods
  6 +
  7 + # returns other instances of this class, which have similar contents
  8 + # like this one. Basically works like this: find out n most interesting
  9 + # (i.e. characteristic) terms from this document, and then build a
  10 + # query from those which is run against the whole index. Which terms
  11 + # are interesting is decided on variour criteria which can be
  12 + # influenced by the given options.
  13 + #
  14 + # The algorithm used here is a quite straight port of the MoreLikeThis class
  15 + # from Apache Lucene.
  16 + #
  17 + # options are:
  18 + # :field_names : Array of field names to use for similarity search (mandatory)
  19 + # :min_term_freq => 2, # Ignore terms with less than this frequency in the source doc.
  20 + # :min_doc_freq => 5, # Ignore words which do not occur in at least this many docs
  21 + # :min_word_length => nil, # Ignore words shorter than this length (longer words tend to
  22 + # be more characteristic for the document they occur in).
  23 + # :max_word_length => nil, # Ignore words if greater than this len.
  24 + # :max_query_terms => 25, # maximum number of terms in the query built
  25 + # :max_num_tokens => 5000, # maximum number of tokens to examine in a single field
  26 + # :boost => false, # when true, a boost according to the relative score of
  27 + # a term is applied to this Term's TermQuery.
  28 + # :similarity => 'DefaultAAFSimilarity' # the similarity implementation to use (the default
  29 + # equals Ferret's internal similarity implementation)
  30 + # :analyzer => 'Ferret::Analysis::StandardAnalyzer' # class name of the analyzer to use
  31 + # :append_to_query => nil # proc taking a query object as argument, which will be called after generating the query. can be used to further manipulate the query used to find related documents, i.e. to constrain the search to a given class in single table inheritance scenarios
  32 + # ferret_options : Ferret options handed over to find_by_contents (i.e. for limits and sorting)
  33 + # ar_options : options handed over to find_by_contents for AR scoping
  34 + def more_like_this(options = {}, ferret_options = {}, ar_options = {})
  35 + options = {
  36 + :field_names => nil, # Default field names
  37 + :min_term_freq => 2, # Ignore terms with less than this frequency in the source doc.
  38 + :min_doc_freq => 5, # Ignore words which do not occur in at least this many docs
  39 + :min_word_length => 0, # Ignore words if less than this len. Default is not to ignore any words.
  40 + :max_word_length => 0, # Ignore words if greater than this len. Default is not to ignore any words.
  41 + :max_query_terms => 25, # maximum number of terms in the query built
  42 + :max_num_tokens => 5000, # maximum number of tokens to analyze when analyzing contents
  43 + :boost => false,
  44 + :similarity => 'ActsAsFerret::MoreLikeThis::DefaultAAFSimilarity', # class name of the similarity implementation to use
  45 + :analyzer => 'Ferret::Analysis::StandardAnalyzer', # class name of the analyzer to use
  46 + :append_to_query => nil,
  47 + :base_class => self.class # base class to use for querying, useful in STI scenarios where BaseClass.find_by_contents can be used to retrieve results from other classes, too
  48 + }.update(options)
  49 + #index.search_each('id:*') do |doc, score|
  50 + # puts "#{doc} == #{index[doc][:description]}"
  51 + #end
  52 + clazz = options[:base_class]
  53 + options[:base_class] = clazz.name
  54 + query = clazz.aaf_index.build_more_like_this_query(self.id, self.class.name, options)
  55 + options[:append_to_query].call(query) if options[:append_to_query]
  56 + clazz.find_by_contents(query, ferret_options, ar_options)
  57 + end
  58 +
  59 + end
  60 +
  61 + module IndexMethods
  62 +
  63 + # TODO to allow morelikethis for unsaved records, we have to give the
  64 + # unsaved record's data to this method. check how this will work out
  65 + # via drb...
  66 + def build_more_like_this_query(id, class_name, options)
  67 + [:similarity, :analyzer].each { |sym| options[sym] = options[sym].constantize.new }
  68 + ferret_index.synchronize do # avoid that concurrent writes close our reader
  69 + ferret_index.send(:ensure_reader_open)
  70 + reader = ferret_index.send(:reader)
  71 + term_freq_map = retrieve_terms(id, class_name, reader, options)
  72 + priority_queue = create_queue(term_freq_map, reader, options)
  73 + create_query(id, class_name, priority_queue, options)
  74 + end
  75 + end
  76 +
  77 + protected
  78 +
  79 + def create_query(id, class_name, priority_queue, options={})
  80 + query = Ferret::Search::BooleanQuery.new
  81 + qterms = 0
  82 + best_score = nil
  83 + while(cur = priority_queue.pop)
  84 + term_query = Ferret::Search::TermQuery.new(cur.field, cur.word)
  85 +
  86 + if options[:boost]
  87 + # boost term according to relative score
  88 + # TODO untested
  89 + best_score ||= cur.score
  90 + term_query.boost = cur.score / best_score
  91 + end
  92 + begin
  93 + query.add_query(term_query, :should)
  94 + rescue Ferret::Search::BooleanQuery::TooManyClauses
  95 + break
  96 + end
  97 + qterms += 1
  98 + break if options[:max_query_terms] > 0 && qterms >= options[:max_query_terms]
  99 + end
  100 + # exclude the original record
  101 + query.add_query(query_for_record(id, class_name), :must_not)
  102 + return query
  103 + end
  104 +
  105 +
  106 +
  107 + # creates a term/term_frequency map for terms from the fields
  108 + # given in options[:field_names]
  109 + def retrieve_terms(id, class_name, reader, options)
  110 + raise "more_like_this atm only works on saved records" if id.nil?
  111 + document_number = document_number(id, class_name) rescue nil
  112 + field_names = options[:field_names]
  113 + max_num_tokens = options[:max_num_tokens]
  114 + term_freq_map = Hash.new(0)
  115 + doc = nil
  116 + record = nil
  117 + field_names.each do |field|
  118 + #puts "field: #{field}"
  119 + term_freq_vector = reader.term_vector(document_number, field) if document_number
  120 + #if false
  121 + if term_freq_vector
  122 + # use stored term vector
  123 + # puts 'using stored term vector'
  124 + term_freq_vector.terms.each do |term|
  125 + term_freq_map[term.text] += term.positions.size unless noise_word?(term.text, options)
  126 + end
  127 + else
  128 + # puts 'no stored term vector'
  129 + # no term vector stored, but we have stored the contents in the index
  130 + # -> extract terms from there
  131 + content = nil
  132 + if document_number
  133 + doc = reader[document_number]
  134 + content = doc[field]
  135 + end
  136 + unless content
  137 + # no term vector, no stored content, so try content from this instance
  138 + record ||= options[:base_class].constantize.find(id)
  139 + content = record.content_for_field_name(field.to_s)
  140 + end
  141 + puts "have doc: #{doc[:id]} with #{field} == #{content}"
  142 + token_count = 0
  143 +
  144 + ts = options[:analyzer].token_stream(field, content)
  145 + while token = ts.next
  146 + break if (token_count+=1) > max_num_tokens
  147 + next if noise_word?(token.text, options)
  148 + term_freq_map[token.text] += 1
  149 + end
  150 + end
  151 + end
  152 + term_freq_map
  153 + end
  154 +
  155 + # create an ordered(by score) list of word,fieldname,score
  156 + # structures
  157 + def create_queue(term_freq_map, reader, options)
  158 + pq = Array.new(term_freq_map.size)
  159 +
  160 + similarity = options[:similarity]
  161 + num_docs = reader.num_docs
  162 + term_freq_map.each_pair do |word, tf|
  163 + # filter out words that don't occur enough times in the source
  164 + next if options[:min_term_freq] && tf < options[:min_term_freq]
  165 +
  166 + # go through all the fields and find the largest document frequency
  167 + top_field = options[:field_names].first
  168 + doc_freq = 0
  169 + options[:field_names].each do |field_name|
  170 + freq = reader.doc_freq(field_name, word)
  171 + if freq > doc_freq
  172 + top_field = field_name
  173 + doc_freq = freq
  174 + end
  175 + end
  176 + # filter out words that don't occur in enough docs
  177 + next if options[:min_doc_freq] && doc_freq < options[:min_doc_freq]
  178 + next if doc_freq == 0 # index update problem ?
  179 +
  180 + idf = similarity.idf(doc_freq, num_docs)
  181 + score = tf * idf
  182 + pq << FrequencyQueueItem.new(word, top_field, score)
  183 + end
  184 + pq.compact!
  185 + pq.sort! { |a,b| a.score<=>b.score }
  186 + return pq
  187 + end
  188 +
  189 + def noise_word?(text, options)
  190 + len = text.length
  191 + (
  192 + (options[:min_word_length] > 0 && len < options[:min_word_length]) ||
  193 + (options[:max_word_length] > 0 && len > options[:max_word_length]) ||
  194 + (options[:stop_words] && options.include?(text))
  195 + )
  196 + end
  197 +
  198 + end
  199 +
  200 + class DefaultAAFSimilarity
  201 + def idf(doc_freq, num_docs)
  202 + return 0.0 if num_docs == 0
  203 + return Math.log(num_docs.to_f/(doc_freq+1)) + 1.0
  204 + end
  205 + end
  206 +
  207 +
  208 + class FrequencyQueueItem
  209 + attr_reader :word, :field, :score
  210 + def initialize(word, field, score)
  211 + @word = word; @field = field; @score = score
  212 + end
  213 + end
  214 +
  215 + end
  216 +end
  217 +
vendor/plugins/acts_as_ferret/lib/multi_index.rb 0 → 100644
@@ -0,0 +1,83 @@ @@ -0,0 +1,83 @@
  1 +module ActsAsFerret #:nodoc:
  2 +
  3 + # this class is not threadsafe
  4 + class MultiIndex
  5 +
  6 + def initialize(model_classes, options = {})
  7 + @model_classes = model_classes
  8 + # ensure all models indexes exist
  9 + @model_classes.each { |m| m.aaf_index.ensure_index_exists }
  10 + default_fields = @model_classes.inject([]) do |fields, c|
  11 + fields + [ c.aaf_configuration[:ferret][:default_field] ].flatten
  12 + end
  13 + @options = {
  14 + :default_field => default_fields
  15 + }.update(options)
  16 + end
  17 +
  18 + def search(query, options={})
  19 + #puts "querystring: #{query.to_s}"
  20 + query = process_query(query)
  21 + #puts "parsed query: #{query.to_s}"
  22 + searcher.search(query, options)
  23 + end
  24 +
  25 + def search_each(query, options = {}, &block)
  26 + query = process_query(query)
  27 + searcher.search_each(query, options, &block)
  28 + end
  29 +
  30 + # checks if all our sub-searchers still are up to date
  31 + def latest?
  32 + return false unless @reader
  33 + # segfaults with 0.10.4 --> TODO report as bug @reader.latest?
  34 + @sub_readers.each do |r|
  35 + return false unless r.latest?
  36 + end
  37 + true
  38 + end
  39 +
  40 + def searcher
  41 + ensure_searcher
  42 + @searcher
  43 + end
  44 +
  45 + def doc(i)
  46 + searcher[i]
  47 + end
  48 + alias :[] :doc
  49 +
  50 + def query_parser
  51 + @query_parser ||= Ferret::QueryParser.new(@options)
  52 + end
  53 +
  54 + def process_query(query)
  55 + query = query_parser.parse(query) if query.is_a?(String)
  56 + return query
  57 + end
  58 +
  59 + def close
  60 + @searcher.close if @searcher
  61 + @reader.close if @reader
  62 + end
  63 +
  64 + protected
  65 +
  66 + def ensure_searcher
  67 + unless latest?
  68 + @sub_readers = @model_classes.map { |clazz|
  69 + begin
  70 + reader = Ferret::Index::IndexReader.new(clazz.aaf_configuration[:index_dir])
  71 + rescue Exception
  72 + raise "error opening #{clazz.aaf_configuration[:index_dir]}: #{$!}"
  73 + end
  74 + }
  75 + close
  76 + @reader = Ferret::Index::IndexReader.new(@sub_readers)
  77 + @searcher = Ferret::Search::Searcher.new(@reader)
  78 + end
  79 + end
  80 +
  81 + end # of class MultiIndex
  82 +
  83 +end
vendor/plugins/acts_as_ferret/lib/remote_index.rb 0 → 100644
@@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
  1 +require 'drb'
  2 +module ActsAsFerret
  3 +
  4 + # This index implementation connects to a remote ferret server instance. It
  5 + # basically forwards all calls to the remote server.
  6 + class RemoteIndex < AbstractIndex
  7 +
  8 + def initialize(config)
  9 + @config = config
  10 + @ferret_config = config[:ferret]
  11 + @server = DRbObject.new(nil, config[:remote])
  12 + end
  13 +
  14 + def method_missing(method_name, *args)
  15 + args.unshift model_class_name
  16 + @server.send(method_name, *args)
  17 + end
  18 +
  19 + def find_id_by_contents(q, options = {}, &proc)
  20 + total_hits, results = @server.find_id_by_contents(model_class_name, q, options)
  21 + block_given? ? yield_results(total_hits, results, &proc) : [ total_hits, results ]
  22 + end
  23 +
  24 + def id_multi_search(query, models, options, &proc)
  25 + total_hits, results = @server.id_multi_search(model_class_name, query, models, options)
  26 + block_given? ? yield_results(total_hits, results, &proc) : [ total_hits, results ]
  27 + end
  28 +
  29 + # add record to index
  30 + def add(record)
  31 + @server.add record.class.name, record.to_doc
  32 + end
  33 + alias << add
  34 +
  35 + private
  36 +
  37 + def yield_results(total_hits, results)
  38 + results.each do |result|
  39 + yield result[:model], result[:id], result[:score], result[:data]
  40 + end
  41 + total_hits
  42 + end
  43 +
  44 + def model_class_name
  45 + @config[:class_name]
  46 + end
  47 +
  48 + end
  49 +
  50 +end
vendor/plugins/acts_as_ferret/lib/shared_index.rb 0 → 100644
@@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
  1 +module ActsAsFerret
  2 +
  3 + class SharedIndex < LocalIndex
  4 +
  5 + # build a ferret query matching only the record with the given id and class
  6 + def query_for_record(id, class_name)
  7 + returning bq = Ferret::Search::BooleanQuery.new do
  8 + bq.add_query(Ferret::Search::TermQuery.new(:id, id.to_s), :must)
  9 + bq.add_query(Ferret::Search::TermQuery.new(:class_name, class_name), :must)
  10 + end
  11 + end
  12 +
  13 + end
  14 +end
vendor/plugins/acts_as_ferret/lib/shared_index_class_methods.rb 0 → 100644
@@ -0,0 +1,90 @@ @@ -0,0 +1,90 @@
  1 +module ActsAsFerret
  2 +
  3 + # class methods for classes using acts_as_ferret :single_index => true
  4 + module SharedIndexClassMethods
  5 +
  6 + def find_id_by_contents(q, options = {}, &block)
  7 + # add class name scoping to query if necessary
  8 + unless options[:models] == :all # search needs to be restricted by one or more class names
  9 + options[:models] ||= []
  10 + # add this class to the list of given models
  11 + options[:models] << self unless options[:models].include?(self)
  12 + # keep original query
  13 + original_query = q
  14 +
  15 + if original_query.is_a? String
  16 + model_query = options[:models].map(&:name).join '|'
  17 + q << %{ +class_name:"#{model_query}"}
  18 + else
  19 + q = Ferret::Search::BooleanQuery.new
  20 + q.add_query(original_query, :must)
  21 + model_query = Ferret::Search::BooleanQuery.new
  22 + options[:models].each do |model|
  23 + model_query.add_query(Ferret::Search::TermQuery.new(:class_name, model.name), :should)
  24 + end
  25 + q.add_query(model_query, :must)
  26 + end
  27 + end
  28 + options.delete :models
  29 +
  30 + super(q, options, &block)
  31 + end
  32 +
  33 + # Overrides the standard find_by_contents for searching a shared index.
  34 + #
  35 + # please note that records from different models will be fetched in
  36 + # separate sql calls, so any sql order_by clause given with
  37 + # find_options[:order] will be ignored.
  38 + def find_by_contents(q, options = {}, find_options = {})
  39 + if order = find_options.delete(:order)
  40 + logger.warn "using a shared index, so ignoring order_by clause #{order}"
  41 + end
  42 + total_hits, result = find_records_lazy_or_not q, options, find_options
  43 + # sort so results have the same order they had when originally retrieved
  44 + # from ferret
  45 + return SearchResults.new(result, total_hits)
  46 + end
  47 +
  48 + protected
  49 +
  50 + def ar_find_by_contents(q, options = {}, find_options = {})
  51 + total_hits, id_arrays = collect_results(q, options)
  52 + result = retrieve_records(id_arrays, find_options)
  53 + result.sort! { |a, b| id_arrays[a.class.name][a.id.to_s].first <=> id_arrays[b.class.name][b.id.to_s].first }
  54 + [ total_hits, result ]
  55 + end
  56 +
  57 + def collect_results(q, options = {})
  58 + id_arrays = {}
  59 + # get object ids for index hits
  60 + rank = 0
  61 + total_hits = find_id_by_contents(q, options) do |model, id, score, data|
  62 + id_arrays[model] ||= {}
  63 + # store result rank and score
  64 + id_arrays[model][id] = [ rank += 1, score ]
  65 + end
  66 + [ total_hits, id_arrays ]
  67 + end
  68 +
  69 +
  70 + # determine all field names in the shared index
  71 + # TODO unused
  72 +# def single_index_field_names(models)
  73 +# @single_index_field_names ||= (
  74 +# searcher = Ferret::Search::Searcher.new(class_index_dir)
  75 +# if searcher.reader.respond_to?(:get_field_names)
  76 +# (searcher.reader.send(:get_field_names) - ['id', 'class_name']).to_a
  77 +# else
  78 +# puts <<-END
  79 +#unable to retrieve field names for class #{self.name}, please
  80 +#consider naming all indexed fields in your call to acts_as_ferret!
  81 +# END
  82 +# models.map { |m| m.content_columns.map { |col| col.name } }.flatten
  83 +# end
  84 +# )
  85 +#
  86 +# end
  87 +
  88 + end
  89 +end
  90 +
vendor/plugins/acts_as_ferret/rakefile 0 → 100644
@@ -0,0 +1,131 @@ @@ -0,0 +1,131 @@
  1 +# rakefile for acts_as_ferret.
  2 +# use to create a gem or generate rdoc api documentation.
  3 +#
  4 +# RELEASE creation:
  5 +# rake release REL=x.y.z
  6 +
  7 +require 'rake'
  8 +require 'rake/rdoctask'
  9 +require 'rake/packagetask'
  10 +require 'rake/gempackagetask'
  11 +require 'rake/testtask'
  12 +require 'rake/contrib/rubyforgepublisher'
  13 +
  14 +def announce(msg='')
  15 + STDERR.puts msg
  16 +end
  17 +
  18 +
  19 +PKG_NAME = 'acts_as_ferret'
  20 +PKG_VERSION = ENV['REL']
  21 +PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
  22 +RUBYFORGE_PROJECT = 'actsasferret'
  23 +RUBYFORGE_USER = 'jkraemer'
  24 +
  25 +desc 'Default: run unit tests.'
  26 +task :default => :test
  27 +
  28 +desc 'Test the acts_as_ferret plugin.'
  29 +Rake::TestTask.new(:test) do |t|
  30 + t.libs << 'lib'
  31 + t.pattern = 'test/**/*_test.rb'
  32 + t.verbose = true
  33 +end
  34 +
  35 +desc 'Generate documentation for the acts_as_ferret plugin.'
  36 +Rake::RDocTask.new(:rdoc) do |rdoc|
  37 + rdoc.rdoc_dir = 'html'
  38 + rdoc.title = "acts_as_ferret - Ferret based full text search for any ActiveRecord model"
  39 + rdoc.options << '--line-numbers' << '--inline-source'
  40 + rdoc.options << '--main' << 'README'
  41 + rdoc.rdoc_files.include('README', 'LICENSE')
  42 + rdoc.template = "#{ENV['template']}.rb" if ENV['template']
  43 + rdoc.rdoc_files.include('lib/**/*.rb')
  44 +end
  45 +
  46 +desc "Publish the API documentation"
  47 +task :pdoc => [:rdoc] do
  48 + Rake::RubyForgePublisher.new(RUBYFORGE_PROJECT, RUBYFORGE_USER).upload
  49 +end
  50 +
  51 +if PKG_VERSION
  52 + spec = Gem::Specification.new do |s|
  53 + s.name = PKG_NAME
  54 + s.version = PKG_VERSION
  55 + s.platform = Gem::Platform::RUBY
  56 + s.summary = "acts_as_ferret - Ferret based full text search for any ActiveRecord model"
  57 + s.files = Dir.glob('**/*', File::FNM_DOTMATCH).reject do |f|
  58 + [ /\.$/, /sqlite$/, /\.log$/, /^pkg/, /\.svn/, /\.\w+\.sw.$/,
  59 + /^html/, /\~$/, /\/\._/, /\/#/ ].any? {|regex| f =~ regex }
  60 + end
  61 + #s.files = FileList["{lib,test}/**/*"].to_a + %w(README MIT-LICENSE CHANGELOG)
  62 + # s.files.delete ...
  63 + s.require_path = 'lib'
  64 + s.autorequire = 'acts_as_ferret'
  65 + s.has_rdoc = true
  66 + # s.test_files = Dir['test/**/*_test.rb']
  67 + s.author = "Jens Kraemer"
  68 + s.email = "jk@jkraemer.net"
  69 + s.homepage = "http://projects.jkraemer.net/acts_as_ferret"
  70 + end
  71 +
  72 + package_task = Rake::GemPackageTask.new(spec) do |pkg|
  73 + pkg.need_tar = true
  74 + end
  75 +
  76 + # Validate that everything is ready to go for a release.
  77 + task :prerelease do
  78 + announce
  79 + announce "**************************************************************"
  80 + announce "* Making RubyGem Release #{PKG_VERSION}"
  81 + announce "**************************************************************"
  82 + announce
  83 + # Are all source files checked in?
  84 + if ENV['RELTEST']
  85 + announce "Release Task Testing, skipping checked-in file test"
  86 + else
  87 + announce "Pulling in svn..."
  88 + `svk pull .`
  89 + announce "Checking for unchecked-in files..."
  90 + data = `svk st`
  91 + unless data =~ /^$/
  92 + fail "SVK status is not clean ... do you have unchecked-in files?"
  93 + end
  94 + announce "No outstanding checkins found ... OK"
  95 + announce "Pushing to svn..."
  96 + `svk push .`
  97 + end
  98 + end
  99 +
  100 +
  101 + desc "tag the new release"
  102 + task :tag => [ :prerelease ] do
  103 + reltag = "REL_#{PKG_VERSION.gsub(/\./, '_')}"
  104 + reltag << ENV['REUSE'].gsub(/\./, '_') if ENV['REUSE']
  105 + announce "Tagging with [#{PKG_VERSION}]"
  106 + if ENV['RELTEST']
  107 + announce "Release Task Testing, skipping tagging"
  108 + else
  109 + `svn copy -m 'tagging version #{PKG_VERSION}' svn://projects.jkraemer.net/acts_as_ferret/trunk/plugin svn://projects.jkraemer.net/acts_as_ferret/tags/#{PKG_VERSION}`
  110 + `svn del -m 'remove old stable' svn://projects.jkraemer.net/acts_as_ferret/tags/stable`
  111 + `svn copy -m 'tagging version #{PKG_VERSION} as stable' svn://projects.jkraemer.net/acts_as_ferret/tags/#{PKG_VERSION} svn://projects.jkraemer.net/acts_as_ferret/tags/stable`
  112 + end
  113 + end
  114 +
  115 + # Upload release to rubyforge
  116 + desc "Upload release to rubyforge"
  117 + task :prel => [ :tag, :prerelease, :package ] do
  118 + `rubyforge login`
  119 + release_command = "rubyforge add_release #{RUBYFORGE_PROJECT} #{PKG_NAME} '#{PKG_VERSION}' pkg/#{PKG_NAME}-#{PKG_VERSION}.gem"
  120 + puts release_command
  121 + system(release_command)
  122 + `rubyforge config #{RUBYFORGE_PROJECT}`
  123 + release_command = "rubyforge add_file #{RUBYFORGE_PROJECT} #{PKG_NAME} '#{PKG_VERSION}' pkg/#{PKG_NAME}-#{PKG_VERSION}.tgz"
  124 + puts release_command
  125 + system(release_command)
  126 + end
  127 +
  128 + desc 'Publish the gem and API docs'
  129 + task :release => [:pdoc, :prel ]
  130 +
  131 +end
vendor/plugins/acts_as_ferret/script/ferret_server 0 → 100644
@@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
  1 +#!/usr/bin/env ruby
  2 +
  3 +# Ferret DRb server launcher script
  4 +#
  5 +# Place doc/ferret_server.yml into RAILS_ROOT/config and fit to taste.
  6 +#
  7 +# Start this script with script/runner and RAILS_ENV set.
  8 +#
  9 +# to run the unit tests against the drb server, start it with
  10 +# RAILS_ENV=test script/runner script/ferret_server
  11 +# and run your tests with the AAF_REMOTE environment variable set to a
  12 +# non-empty value
  13 +
  14 +
  15 +ActsAsFerret::Remote::Server.start
  16 +DRb.thread.join
  17 +
  18 +
vendor/plugins/acts_as_ferret/script/ferret_start 0 → 100755
@@ -0,0 +1,72 @@ @@ -0,0 +1,72 @@
  1 +#!/usr/bin/env ruby
  2 +# Ferret DRb server launcher script
  3 +#
  4 +# Place doc/ferret_server.yml into RAILS_ROOT/config and fit to taste. Start
  5 +# it with RAILS_ENV set to the desired environment.
  6 +#
  7 +#
  8 +# To run the demo project's unit tests against the drb server, start it with
  9 +#
  10 +# RAILS_ENV=test script/ferret_start
  11 +#
  12 +# and run your tests with the AAF_REMOTE environment variable set to a
  13 +# non-empty value:
  14 +#
  15 +# AAF_REMOTE=true rake
  16 +#
  17 +# The server writes a log file in log/ferret_server.log, it's
  18 +# STDOUT gets redirected to log/ferret_server.out
  19 +
  20 +ENV['FERRET_USE_LOCAL_INDEX'] = 'true'
  21 +require File.dirname(__FILE__) + '/../config/boot'
  22 +require RAILS_ROOT + '/config/environment'
  23 +
  24 +
  25 +config = ActsAsFerret::Remote::Config.load
  26 +@pid_file = config['pid_file']
  27 +
  28 +def write_pid_file
  29 + raise "No PID file defined" if @pid_file.blank?
  30 + open(@pid_file,"w") {|f| f.write(Process.pid) }
  31 +end
  32 +
  33 +def safefork
  34 + tryagain = true
  35 +
  36 + while tryagain
  37 + tryagain = false
  38 + begin
  39 + if pid = fork
  40 + return pid
  41 + end
  42 + rescue Errno::EWOULDBLOCK
  43 + sleep 5
  44 + tryagain = true
  45 + end
  46 + end
  47 +end
  48 +
  49 +safefork and exit
  50 +at_exit do
  51 + File.unlink(@pid_file) if @pid_file && File.exists?(@pid_file) && File.read(@pid_file).to_i == Process.pid
  52 +end
  53 +print "Starting ferret DRb server..."
  54 +trap("TERM") { exit(0) }
  55 +sess_id = Process.setsid
  56 +
  57 +
  58 +begin
  59 + ActsAsFerret::Remote::Server.start
  60 + write_pid_file
  61 + puts "Done."
  62 + STDIN.reopen "/dev/null" # Free file descriptors and
  63 + STDOUT.reopen "#{RAILS_ROOT}/log/ferret_server.out", "a" # point them somewhere sensible
  64 + STDERR.reopen STDOUT # STDOUT/STDERR should go to a logfile
  65 +rescue
  66 + $stderr.puts "Error starting ferret DRb server: #{$!}"
  67 + $stderr.puts $!.backtrace
  68 + exit(1)
  69 +end
  70 +DRb.thread.join
  71 +
  72 +# vim:set filetype=ruby:
vendor/plugins/acts_as_ferret/script/ferret_stop 0 → 100755
@@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
  1 +#!/usr/bin/env script/runner
  2 +
  3 +config = ActsAsFerret::Remote::Config.load
  4 +
  5 +def send_signal(signal, pid_file)
  6 + pid = open(pid_file).read.to_i
  7 + print "Sending #{signal} to ferret_server with PID #{pid}..."
  8 + begin
  9 + Process.kill(signal, pid)
  10 + rescue Errno::ESRCH
  11 + puts "Process does not exist. Not running. Removing stale pid file anyway."
  12 + File.unlink(pid_file)
  13 + end
  14 +
  15 + puts "Done."
  16 +end
  17 +
  18 +pid_file = config['pid_file']
  19 +puts "Stopping ferret_server..."
  20 +if File.file?(pid_file)
  21 + send_signal("TERM", pid_file)
  22 +else
  23 + puts "no pid file found"
  24 +end
  25 +
  26 +# vim:set filetype=ruby:
vendor/plugins/acts_as_taggable_on_steroids/CHANGELOG 0 → 100644
@@ -0,0 +1,58 @@ @@ -0,0 +1,58 @@
  1 +[1 July 2007]
  2 +
  3 +* Fix incorrect tagging when the case of the tag list is changed.
  4 +
  5 +* Fix deprecated Tag.delimiter accessor.
  6 +
  7 +[23 June 2007]
  8 +
  9 +* Add validation to Tag model.
  10 +
  11 +* find_options_for_tagged_with should always return a hash.
  12 +
  13 +* find_tagged_with passing in no tags should return an empty array.
  14 +
  15 +* Improve compatibility with PostgreSQL.
  16 +
  17 +[21 June 2007]
  18 +
  19 +* Remove extra .rb from generated migration file name.
  20 +
  21 +[15 June 2007]
  22 +
  23 +* Introduce TagList class.
  24 +
  25 +* Various cleanups and improvements.
  26 +
  27 +* Use TagList.delimiter now, not Tag.delimiter. Tag.delimiter will be removed at some stage.
  28 +
  29 +[11 June 2007]
  30 +
  31 +* Restructure the creation of the options for find_tagged_with [Thijs Cadier]
  32 +
  33 +* Add an example migration with a generator.
  34 +
  35 +* Add caching.
  36 +
  37 +* Fix compatibility with Ruby < 1.8.6
  38 +
  39 +[23 April 2007]
  40 +
  41 +* Make tag_list to respect Tag.delimiter
  42 +
  43 +[31 March 2007]
  44 +
  45 +* Add Tag.delimiter accessor to change how tags are parsed.
  46 +* Fix :include => :tags when used with find_tagged_with
  47 +
  48 +[7 March 2007]
  49 +
  50 +* Fix tag_counts for SQLServer [Brad Young]
  51 +
  52 +[21 Feb 2007]
  53 +
  54 +* Use scoping instead of TagCountsExtension [Michael Schuerig]
  55 +
  56 +[7 Jan 2007]
  57 +
  58 +* Add :match_all to find_tagged_with [Michael Sheakoski]
vendor/plugins/acts_as_taggable_on_steroids/MIT-LICENSE 0 → 100644
@@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
  1 +Copyright (c) 2006 Jonathan Viney
  2 +
  3 +Permission is hereby granted, free of charge, to any person obtaining
  4 +a copy of this software and associated documentation files (the
  5 +"Software"), to deal in the Software without restriction, including
  6 +without limitation the rights to use, copy, modify, merge, publish,
  7 +distribute, sublicense, and/or sell copies of the Software, and to
  8 +permit persons to whom the Software is furnished to do so, subject to
  9 +the following conditions:
  10 +
  11 +The above copyright notice and this permission notice shall be
  12 +included in all copies or substantial portions of the Software.
  13 +
  14 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  15 +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  16 +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  17 +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  18 +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  19 +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  20 +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
vendor/plugins/acts_as_taggable_on_steroids/README 0 → 100644
@@ -0,0 +1,116 @@ @@ -0,0 +1,116 @@
  1 += acts_as_taggable_on_steroids
  2 +
  3 +If you find this plugin useful, please consider a donation to show your support!
  4 +
  5 + http://www.paypal.com/cgi-bin/webscr?cmd=_send-money
  6 +
  7 + Email address: jonathan.viney@gmail.com
  8 +
  9 +== Instructions
  10 +
  11 +This plugin is based on acts_as_taggable by DHH but includes extras
  12 +such as tests, smarter tag assignment, and tag cloud calculations.
  13 +
  14 +Thanks to www.fanacious.com for allowing this plugin to be released. Please check out
  15 +their site to show your support.
  16 +
  17 +== Resources
  18 +
  19 +Install
  20 + * script/plugin install http://svn.viney.net.nz/things/rails/plugins/acts_as_taggable_on_steroids
  21 +
  22 +== Usage
  23 +
  24 +=== Prepare database
  25 +
  26 +Generate and apply the migration:
  27 +
  28 + ruby script/generate acts_as_taggable_migration
  29 + rake db:migrate
  30 +
  31 +=== Basic tagging
  32 +
  33 +Using the examples from the tests, let's suppose we have users that have many posts and we want those
  34 +posts to be able to be tagged by the user.
  35 +
  36 +As usual, we add +acts_as_taggable+ to the Post class:
  37 +
  38 + class Post < ActiveRecord::Base
  39 + acts_as_taggable
  40 +
  41 + belongs_to :user
  42 + end
  43 +
  44 +We can now use the tagging methods provided by acts_as_taggable, <tt>tag_list</tt> and <tt>tag_list=</tt>. Both these
  45 +methods work like regular attribute accessors.
  46 +
  47 + p = Post.find(:first)
  48 + p.tag_list.to_s # ""
  49 + p.tag_list = "Funny, Silly"
  50 + p.save
  51 + p.reload.tag_list.to_s # "Funny, Silly"
  52 +
  53 +You can also add or remove arrays of tags.
  54 +
  55 + p.tag_list.add("Great", "Awful")
  56 + p.tag_list.remove("Funny")
  57 +
  58 +=== Finding tagged objects
  59 +
  60 +To retrieve objects tagged with a certain tag, use find_tagged_with.
  61 +
  62 + Post.find_tagged_with('Funny, Silly')
  63 +
  64 +By default, find_tagged_with will find objects that have any of the given tags. To
  65 +find only objects that are tagged with all the given tags, use match_all.
  66 +
  67 + Post.find_tagged_with('Funny, Silly', :match_all => true)
  68 +
  69 +=== Tag cloud calculations
  70 +
  71 +To construct tag clouds, the frequency of each tag needs to be calculated.
  72 +Because we specified +acts_as_taggable+ on the <tt>Post</tt> class, we can
  73 +get a calculation of all the tag counts by using <tt>Post.tag_counts</tt>. But what if we wanted a tag count for
  74 +an single user's posts? To achieve this we call tag_counts on the association:
  75 +
  76 + User.find(:first).posts.tag_counts
  77 +
  78 +=== Caching
  79 +
  80 +It is useful to cache the list of tags to reduce the number of queries executed. To do this,
  81 +add a column named <tt>cached_tag_list</tt> to the model which is being tagged.
  82 +
  83 + class CachePostTagList < ActiveRecord::Migration
  84 + def self.up
  85 + # You should make sure that the column is long enough to hold
  86 + # the full tag list. In some situations the :text type may be more appropriate.
  87 + add_column :posts, :cached_tag_list, :string
  88 + end
  89 + end
  90 +
  91 + class Post < ActiveRecord::Base
  92 + acts_as_taggable
  93 +
  94 + # The caching column defaults to cached_tag_list, but can be changed:
  95 + #
  96 + # set_cached_tag_list_column_name "my_caching_column_name"
  97 + end
  98 +
  99 +The details of the caching are handled for you. Just continue to use the tag_list accessor as you normally would.
  100 +Note that the cached tag list will not be updated if you directly create Tagging objects or manually append to the
  101 +<tt>tags</tt> or <tt>taggings</tt> associations. To update the cached tag list you should call <tt>save_cached_tag_list</tt> manually.
  102 +
  103 +=== Delimiter
  104 +
  105 +If you want to change the delimiter used to parse and present tags, set TagList.delimiter.
  106 +For example, to use spaces instead of commas, add the following to config/environment.rb:
  107 +
  108 + TagList.delimiter = " "
  109 +
  110 +=== Other
  111 +
  112 +Problems, comments, and suggestions all welcome. jonathan.viney@gmail.com
  113 +
  114 +== Credits
  115 +
  116 +www.fanacious.com
vendor/plugins/acts_as_taggable_on_steroids/Rakefile 0 → 100644
@@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
  1 +require 'rake'
  2 +require 'rake/testtask'
  3 +require 'rake/rdoctask'
  4 +
  5 +desc 'Default: run unit tests.'
  6 +task :default => :test
  7 +
  8 +desc 'Test the acts_as_taggable_on_steroids plugin.'
  9 +Rake::TestTask.new(:test) do |t|
  10 + t.libs << 'lib'
  11 + t.pattern = 'test/**/*_test.rb'
  12 + t.verbose = true
  13 +end
  14 +
  15 +desc 'Generate documentation for the acts_as_taggable_on_steroids plugin.'
  16 +Rake::RDocTask.new(:rdoc) do |rdoc|
  17 + rdoc.rdoc_dir = 'rdoc'
  18 + rdoc.title = 'Acts As Taggable On Steroids'
  19 + rdoc.options << '--line-numbers' << '--inline-source'
  20 + rdoc.rdoc_files.include('README')
  21 + rdoc.rdoc_files.include('lib/**/*.rb')
  22 +end
vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/acts_as_taggable_migration_generator.rb 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 +class ActsAsTaggableMigrationGenerator < Rails::Generator::Base
  2 + def manifest
  3 + record do |m|
  4 + m.migration_template 'migration.rb', 'db/migrate'
  5 + end
  6 + end
  7 +
  8 + def file_name
  9 + "acts_as_taggable_migration"
  10 + end
  11 +end
vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/templates/migration.rb 0 → 100644
@@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
  1 +class ActsAsTaggableMigration < ActiveRecord::Migration
  2 + def self.up
  3 + create_table :tags do |t|
  4 + t.column :name, :string
  5 + end
  6 +
  7 + create_table :taggings do |t|
  8 + t.column :tag_id, :integer
  9 + t.column :taggable_id, :integer
  10 +
  11 + # You should make sure that the column created is
  12 + # long enough to store the required class names.
  13 + t.column :taggable_type, :string
  14 +
  15 + t.column :created_at, :datetime
  16 + end
  17 +
  18 + add_index :taggings, :tag_id
  19 + add_index :taggings, [:taggable_id, :taggable_type]
  20 + end
  21 +
  22 + def self.down
  23 + drop_table :taggings
  24 + drop_table :tags
  25 + end
  26 +end
vendor/plugins/acts_as_taggable_on_steroids/init.rb 0 → 100644
@@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
  1 +require File.dirname(__FILE__) + '/lib/acts_as_taggable'
  2 +
  3 +require File.dirname(__FILE__) + '/lib/tagging'
  4 +require File.dirname(__FILE__) + '/lib/tag'
vendor/plugins/acts_as_taggable_on_steroids/lib/acts_as_taggable.rb 0 → 100644
@@ -0,0 +1,145 @@ @@ -0,0 +1,145 @@
  1 +module ActiveRecord
  2 + module Acts #:nodoc:
  3 + module Taggable #:nodoc:
  4 + def self.included(base)
  5 + base.extend(ClassMethods)
  6 + end
  7 +
  8 + module ClassMethods
  9 + def acts_as_taggable(options = {})
  10 + has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
  11 + has_many :tags, :through => :taggings
  12 +
  13 + before_save :save_cached_tag_list
  14 + after_save :save_tags
  15 +
  16 + include ActiveRecord::Acts::Taggable::InstanceMethods
  17 + extend ActiveRecord::Acts::Taggable::SingletonMethods
  18 +
  19 + alias_method :reload_without_tag_list, :reload
  20 + alias_method :reload, :reload_with_tag_list
  21 + end
  22 +
  23 + def cached_tag_list_column_name
  24 + "cached_tag_list"
  25 + end
  26 +
  27 + def set_cached_tag_list_column_name(value = nil, &block)
  28 + define_attr_method :cached_tag_list_column_name, value, &block
  29 + end
  30 + end
  31 +
  32 + module SingletonMethods
  33 + # Pass either a tag string, or an array of strings or tags
  34 + #
  35 + # Options:
  36 + # :exclude - Find models that are not tagged with the given tags
  37 + # :match_all - Find models that match all of the given tags, not just one
  38 + # :conditions - A piece of SQL conditions to add to the query
  39 + def find_tagged_with(tags, options = {})
  40 + tags = TagList.from(tags).names
  41 + return [] if tags.empty?
  42 +
  43 + conditions = tags.map {|t| sanitize_sql(["tags.name LIKE ?",t])}.join(' OR ')
  44 + conditions += ' AND ' + sanitize_sql(options.delete(:conditions)) if options[:conditions]
  45 + group = "#{table_name}.id HAVING COUNT(#{table_name}.id) = #{tags.size}" if options.delete(:match_all)
  46 + exclude = options.delete(:exclude)
  47 + taggeds = find(:all, {:conditions => conditions, :include => :tags, :group => group}.update(options))
  48 + exclude ? find(:all) - taggeds : taggeds
  49 + end
  50 +
  51 + # Options:
  52 + # :start_at - Restrict the tags to those created after a certain time
  53 + # :end_at - Restrict the tags to those created before a certain time
  54 + # :conditions - A piece of SQL conditions to add to the query
  55 + # :limit - The maximum number of tags to return
  56 + # :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
  57 + # :at_least - Exclude tags with a frequency less than the given value
  58 + # :at_most - Exclude tags with a frequency greater then the given value
  59 + def tag_counts(*args)
  60 + Tag.find(:all, find_options_for_tag_counts(*args))
  61 + end
  62 +
  63 + def find_options_for_tag_counts(options = {})
  64 + options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit
  65 +
  66 + scope = scope(:find)
  67 + start_at = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options[:start_at]]) if options[:start_at]
  68 + end_at = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options[:end_at]]) if options[:end_at]
  69 +
  70 + conditions = [
  71 + "#{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)}",
  72 + options[:conditions],
  73 + scope && scope[:conditions],
  74 + start_at,
  75 + end_at
  76 + ]
  77 + conditions = conditions.compact.join(' and ')
  78 +
  79 + at_least = sanitize_sql(['COUNT(*) >= ?', options[:at_least]]) if options[:at_least]
  80 + at_most = sanitize_sql(['COUNT(*) <= ?', options[:at_most]]) if options[:at_most]
  81 + having = [at_least, at_most].compact.join(' and ')
  82 + group_by = "#{Tag.table_name}.id, #{Tag.table_name}.name HAVING COUNT(*) > 0"
  83 + group_by << " AND #{having}" unless having.blank?
  84 +
  85 + { :select => "#{Tag.table_name}.id, #{Tag.table_name}.name, COUNT(*) AS count",
  86 + :joins => "LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id LEFT OUTER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id",
  87 + :conditions => conditions,
  88 + :group => group_by,
  89 + :order => options[:order],
  90 + :limit => options[:limit]
  91 + }
  92 + end
  93 + end
  94 +
  95 + module InstanceMethods
  96 + def tag_list
  97 + if @tag_list
  98 + @tag_list
  99 + elsif caching_tag_list? and !send(self.class.cached_tag_list_column_name).nil?
  100 + @tag_list = TagList.from(send(self.class.cached_tag_list_column_name))
  101 + else
  102 + @tag_list = TagList.new(tags.map(&:name))
  103 + end
  104 + end
  105 +
  106 + def tag_list=(value)
  107 + @tag_list = TagList.from(value)
  108 + end
  109 +
  110 + def save_cached_tag_list
  111 + if caching_tag_list? and !tag_list.blank?
  112 + self[self.class.cached_tag_list_column_name] = tag_list.to_s
  113 + end
  114 + end
  115 +
  116 + def save_tags
  117 + return unless @tag_list
  118 +
  119 + new_tag_names = @tag_list.names - tags.map(&:name)
  120 + old_tags = tags.reject { |tag| @tag_list.names.include?(tag.name) }
  121 +
  122 + self.class.transaction do
  123 + tags.delete(*old_tags) if old_tags.any?
  124 +
  125 + new_tag_names.each do |new_tag_name|
  126 + tags << (Tag.find(:first, :conditions => ['name like ?',new_tag_name]) || Tag.create(:name => new_tag_name))
  127 + end
  128 + end
  129 + true
  130 + end
  131 +
  132 + def reload_with_tag_list(*args)
  133 + @tag_list = nil
  134 + reload_without_tag_list(*args)
  135 + end
  136 +
  137 + def caching_tag_list?
  138 + self.class.column_names.include?(self.class.cached_tag_list_column_name)
  139 + end
  140 + end
  141 + end
  142 + end
  143 +end
  144 +
  145 +ActiveRecord::Base.send(:include, ActiveRecord::Acts::Taggable)
vendor/plugins/acts_as_taggable_on_steroids/lib/tag.rb 0 → 100644
@@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
  1 +class Tag < ActiveRecord::Base
  2 + has_many :taggings
  3 +
  4 + validates_presence_of :name
  5 + validates_uniqueness_of :name
  6 +
  7 + class << self
  8 + delegate :delimiter, :delimiter=, :to => TagList
  9 + end
  10 +
  11 + def ==(object)
  12 + super || (object.is_a?(Tag) && name == object.name)
  13 + end
  14 +
  15 + def to_s
  16 + name
  17 + end
  18 +
  19 + def count
  20 + read_attribute(:count).to_i
  21 + end
  22 +
  23 + def self.hierarchical=(bool)
  24 + if bool
  25 + acts_as_tree
  26 + end
  27 + end
  28 +
  29 + # All the tags that can be a new parent for this tag, that is all but itself and its descendents to avoid loops
  30 + def parent_candidates
  31 + Tag.find_all_by_pending(false) - descendents - [self]
  32 + end
  33 +
  34 + # All tags that have this tag as its one of its ancestors
  35 + def descendents
  36 + children.to_a.sum([], &:descendents) + children
  37 + end
  38 +
  39 +end
vendor/plugins/acts_as_taggable_on_steroids/lib/tag_counts_extension.rb 0 → 100644
@@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
  1 +module TagCountsExtension
  2 +end
vendor/plugins/acts_as_taggable_on_steroids/lib/tag_list.rb 0 → 100644
@@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
  1 +class TagList
  2 + cattr_accessor :delimiter
  3 + self.delimiter = ','
  4 +
  5 + attr_reader :names
  6 +
  7 + def initialize(*names)
  8 + @names = []
  9 + add(*names)
  10 + end
  11 +
  12 + def add(*names)
  13 + names = names.flatten
  14 +
  15 + # Strip whitespace and remove blank or duplicate tags
  16 + names.map!(&:strip)
  17 + names.reject!(&:blank?)
  18 +
  19 + @names.concat(names)
  20 + @names.uniq!
  21 + end
  22 +
  23 + def remove(*names)
  24 + names = names.flatten
  25 + @names.delete_if { |name| names.include?(name) }
  26 + end
  27 +
  28 + def blank?
  29 + @names.empty?
  30 + end
  31 +
  32 + def to_s
  33 + @names.map do |name|
  34 + name.include?(delimiter) ? "\"#{name}\"" : name
  35 + end.join(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ")
  36 + end
  37 +
  38 + def ==(other)
  39 + super || (other.is_a?(TagList) && other.names == @names)
  40 + end
  41 +
  42 + class << self
  43 + def from(tags)
  44 + case tags
  45 + when String
  46 + new(parse(tags))
  47 + when Array
  48 + new(tags.map(&:to_s))
  49 + else
  50 + new([])
  51 + end
  52 + end
  53 +
  54 + def parse(string)
  55 + returning [] do |names|
  56 + string = string.to_s.dup
  57 +
  58 + # Parse the quoted tags
  59 + string.gsub!(/"(.*?)"\s*#{delimiter}?\s*/) { names << $1; "" }
  60 + string.gsub!(/'(.*?)'\s*#{delimiter}?\s*/) { names << $1; "" }
  61 +
  62 + names.concat(string.split(delimiter))
  63 + end
  64 + end
  65 + end
  66 +end
vendor/plugins/acts_as_taggable_on_steroids/lib/tagging.rb 0 → 100644
@@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
  1 +class Tagging < ActiveRecord::Base
  2 + belongs_to :tag
  3 + belongs_to :taggable, :polymorphic => true
  4 +end
vendor/plugins/acts_as_taggable_on_steroids/test/abstract_unit.rb 0 → 100644
@@ -0,0 +1,82 @@ @@ -0,0 +1,82 @@
  1 +require 'test/unit'
  2 +
  3 +begin
  4 + require File.dirname(__FILE__) + '/../../../../config/environment'
  5 +rescue LoadError
  6 + require 'rubygems'
  7 + require_gem 'activerecord'
  8 + require_gem 'actionpack'
  9 +end
  10 +
  11 +# Search for fixtures first
  12 +fixture_path = File.dirname(__FILE__) + '/fixtures/'
  13 +begin
  14 + Dependencies.load_paths.insert(0, fixture_path)
  15 +rescue
  16 + $LOAD_PATH.unshift(fixture_path)
  17 +end
  18 +
  19 +require 'active_record/fixtures'
  20 +
  21 +require File.dirname(__FILE__) + '/../lib/acts_as_taggable'
  22 +require_dependency File.dirname(__FILE__) + '/../lib/tag_list'
  23 +
  24 +ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + '/debug.log')
  25 +#ActiveRecord::Base.configurations = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
  26 +#ActiveRecord::Base.establish_connection(ENV['DB'] || 'mysql')
  27 +
  28 +load(File.dirname(__FILE__) + '/schema.rb')
  29 +
  30 +Test::Unit::TestCase.fixture_path = fixture_path
  31 +
  32 +class Test::Unit::TestCase #:nodoc:
  33 + self.use_transactional_fixtures = true
  34 + self.use_instantiated_fixtures = false
  35 +
  36 + def assert_equivalent(expected, actual, message = nil)
  37 + if expected.first.is_a?(ActiveRecord::Base)
  38 + assert_equal expected.sort_by(&:id), actual.sort_by(&:id), message
  39 + else
  40 + assert_equal expected.sort, actual.sort, message
  41 + end
  42 + end
  43 +
  44 + def assert_tag_counts(tags, expected_values)
  45 + # Map the tag fixture names to real tag names
  46 + expected_values = expected_values.inject({}) do |hash, (tag, count)|
  47 + hash[tags(tag).name] = count
  48 + hash
  49 + end
  50 +
  51 + tags.each do |tag|
  52 + value = expected_values.delete(tag.name)
  53 + assert_not_nil value, "Expected count for #{tag.name} was not provided" if value.nil?
  54 + assert_equal value, tag.count, "Expected value of #{value} for #{tag.name}, but was #{tag.count}"
  55 + end
  56 +
  57 + unless expected_values.empty?
  58 + assert false, "The following tag counts were not present: #{expected_values.inspect}"
  59 + end
  60 + end
  61 +
  62 + def assert_queries(num = 1)
  63 + $query_count = 0
  64 + yield
  65 + ensure
  66 + assert_equal num, $query_count, "#{$query_count} instead of #{num} queries were executed."
  67 + end
  68 +
  69 + def assert_no_queries(&block)
  70 + assert_queries(0, &block)
  71 + end
  72 +end
  73 +
  74 +ActiveRecord::Base.connection.class.class_eval do
  75 + def execute_with_counting(sql, name = nil, &block)
  76 + $query_count ||= 0
  77 + $query_count += 1
  78 + execute_without_counting(sql, name, &block)
  79 + end
  80 +
  81 + alias_method_chain :execute, :counting
  82 +end
vendor/plugins/acts_as_taggable_on_steroids/test/acts_as_taggable_test.rb 0 → 100644
@@ -0,0 +1,272 @@ @@ -0,0 +1,272 @@
  1 +require File.dirname(__FILE__) + '/abstract_unit'
  2 +
  3 +class ActsAsTaggableOnSteroidsTest < Test::Unit::TestCase
  4 + fixtures :tags, :taggings, :posts, :users, :photos
  5 +
  6 + def test_find_tagged_with
  7 + assert_equivalent [posts(:jonathan_sky), posts(:sam_flowers)], Post.find_tagged_with('"Very good"')
  8 + assert_equal Post.find_tagged_with('"Very good"'), Post.find_tagged_with(['Very good'])
  9 + assert_equal Post.find_tagged_with('"Very good"'), Post.find_tagged_with([tags(:good)])
  10 +
  11 + assert_equivalent [photos(:jonathan_dog), photos(:sam_flower), photos(:sam_sky)], Photo.find_tagged_with('Nature')
  12 + assert_equal Photo.find_tagged_with('Nature'), Photo.find_tagged_with(['Nature'])
  13 + assert_equal Photo.find_tagged_with('Nature'), Photo.find_tagged_with([tags(:nature)])
  14 +
  15 + assert_equivalent [photos(:jonathan_bad_cat), photos(:jonathan_dog), photos(:jonathan_questioning_dog)], Photo.find_tagged_with('"Crazy animal" Bad')
  16 + assert_equal Photo.find_tagged_with('"Crazy animal" Bad'), Photo.find_tagged_with(['Crazy animal', 'Bad'])
  17 + assert_equal Photo.find_tagged_with('"Crazy animal" Bad'), Photo.find_tagged_with([tags(:animal), tags(:bad)])
  18 + end
  19 +
  20 + def test_find_tagged_with_nothing
  21 + assert_equal [], Post.find_tagged_with("")
  22 + assert_equal [], Post.find_tagged_with([])
  23 + end
  24 +
  25 + def test_find_tagged_with_nonexistant_tags
  26 + assert_equal [], Post.find_tagged_with('ABCDEFG')
  27 + assert_equal [], Photo.find_tagged_with(['HIJKLM'])
  28 + assert_equal [], Photo.find_tagged_with([Tag.new(:name => 'unsaved tag')])
  29 + end
  30 +
  31 + def test_find_tagged_with_matching_all_tags
  32 + assert_equivalent [photos(:jonathan_dog)], Photo.find_tagged_with('Crazy animal, "Nature"', :match_all => true)
  33 + assert_equivalent [posts(:jonathan_sky), posts(:sam_flowers)], Post.find_tagged_with(['Very good', 'Nature'], :match_all => true)
  34 + end
  35 +
  36 + def test_find_tagged_with_exclusions
  37 + assert_equivalent [photos(:jonathan_questioning_dog), photos(:jonathan_bad_cat)], Photo.find_tagged_with("Nature", :exclude => true)
  38 + assert_equivalent [posts(:jonathan_grass), posts(:jonathan_rain)], Post.find_tagged_with("'Very good', Bad", :exclude => true)
  39 + end
  40 +
  41 +# def test_find_options_for_tagged_with_no_tags_returns_empty_hash
  42 +# assert_equal Hash.new, Post.find_options_for_tagged_with("")
  43 +# assert_equal Hash.new, Post.find_options_for_tagged_with([nil])
  44 +# end
  45 +
  46 +# def test_find_options_for_tagged_with_leavs_arguments_unchanged
  47 +# original_tags = photos(:jonathan_questioning_dog).tags.dup
  48 +# Photo.find_options_for_tagged_with(photos(:jonathan_questioning_dog).tags)
  49 +# assert_equal original_tags, photos(:jonathan_questioning_dog).tags
  50 +# end
  51 +
  52 +# def test_find_options_for_tagged_with_respects_custom_table_name
  53 +# Tagging.table_name = "categorisations"
  54 +# Tag.table_name = "categories"
  55 +#
  56 +# options = Photo.find_options_for_tagged_with("Hello")
  57 +#
  58 +# assert_no_match Regexp.new(" taggings "), options[:joins]
  59 +# assert_no_match Regexp.new(" tags "), options[:joins]
  60 +#
  61 +# assert_match Regexp.new(" categorisations "), options[:joins]
  62 +# assert_match Regexp.new(" categories "), options[:joins]
  63 +# ensure
  64 +# Tagging.table_name = "taggings"
  65 +# Tag.table_name = "tags"
  66 +# end
  67 +
  68 + def test_include_tags_on_find_tagged_with
  69 + assert_nothing_raised do
  70 + Photo.find_tagged_with('Nature', :include => :tags)
  71 + Photo.find_tagged_with("Nature", :include => { :taggings => :tag })
  72 + end
  73 + end
  74 +
  75 + def test_basic_tag_counts_on_class
  76 + assert_tag_counts Post.tag_counts, :good => 2, :nature => 5, :question => 1, :bad => 1
  77 + assert_tag_counts Photo.tag_counts, :good => 1, :nature => 3, :question => 1, :bad => 1, :animal => 3
  78 + end
  79 +
  80 + def test_tag_counts_on_class_with_date_conditions
  81 + assert_tag_counts Post.tag_counts(:start_at => Date.new(2006, 8, 4)), :good => 1, :nature => 3, :question => 1, :bad => 1
  82 + assert_tag_counts Post.tag_counts(:end_at => Date.new(2006, 8, 6)), :good => 1, :nature => 4, :question => 1
  83 + assert_tag_counts Post.tag_counts(:start_at => Date.new(2006, 8, 5), :end_at => Date.new(2006, 8, 8)), :good => 1, :nature => 2, :bad => 1
  84 +
  85 + assert_tag_counts Photo.tag_counts(:start_at => Date.new(2006, 8, 12), :end_at => Date.new(2006, 8, 17)), :good => 1, :nature => 1, :bad => 1, :question => 1, :animal => 2
  86 + end
  87 +
  88 + def test_tag_counts_on_class_with_frequencies
  89 + assert_tag_counts Photo.tag_counts(:at_least => 2), :nature => 3, :animal => 3
  90 + assert_tag_counts Photo.tag_counts(:at_most => 2), :good => 1, :question => 1, :bad => 1
  91 + end
  92 +
  93 + def test_tag_counts_with_limit
  94 + assert_equal 2, Photo.tag_counts(:limit => 2).size
  95 + assert_equal 1, Post.tag_counts(:at_least => 4, :limit => 2).size
  96 + end
  97 +
  98 + def test_tag_counts_with_limit_and_order
  99 + assert_equal [tags(:nature), tags(:good)], Post.tag_counts(:order => 'count desc', :limit => 2)
  100 + end
  101 +
  102 + def test_tag_counts_on_association
  103 + assert_tag_counts users(:jonathan).posts.tag_counts, :good => 1, :nature => 3, :question => 1
  104 + assert_tag_counts users(:sam).posts.tag_counts, :good => 1, :nature => 2, :bad => 1
  105 +
  106 + assert_tag_counts users(:jonathan).photos.tag_counts, :animal => 3, :nature => 1, :question => 1, :bad => 1
  107 + assert_tag_counts users(:sam).photos.tag_counts, :nature => 2, :good => 1
  108 + end
  109 +
  110 + def test_tag_counts_on_association_with_options
  111 + assert_equal [], users(:jonathan).posts.tag_counts(:conditions => '1=0')
  112 + assert_tag_counts users(:jonathan).posts.tag_counts(:at_most => 2), :good => 1, :question => 1
  113 + end
  114 +
  115 + def test_tag_counts_respects_custom_table_names
  116 + Tagging.table_name = "categorisations"
  117 + Tag.table_name = "categories"
  118 +
  119 + options = Photo.find_options_for_tag_counts(:start_at => 2.weeks.ago, :end_at => Date.today)
  120 + sql = options.values.join(' ')
  121 +
  122 + assert_no_match /taggings/, sql
  123 + assert_no_match /tags/, sql
  124 +
  125 + assert_match /categorisations/, sql
  126 + assert_match /categories/, sql
  127 + ensure
  128 + Tagging.table_name = "taggings"
  129 + Tag.table_name = "tags"
  130 + end
  131 +
  132 + def test_tag_list_reader
  133 + assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list.names
  134 + assert_equivalent ["Bad", "Crazy animal"], photos(:jonathan_bad_cat).tag_list.names
  135 + end
  136 +
  137 + def test_reassign_tag_list
  138 + assert_equivalent ["Nature", "Question"], posts(:jonathan_rain).tag_list.names
  139 + posts(:jonathan_rain).taggings.reload
  140 +
  141 + # Only an update of the posts table should be executed
  142 + assert_queries 1 do
  143 + posts(:jonathan_rain).update_attributes!(:tag_list => posts(:jonathan_rain).tag_list.to_s)
  144 + end
  145 +
  146 + assert_equivalent ["Nature", "Question"], posts(:jonathan_rain).tag_list.names
  147 + end
  148 +
  149 + def test_new_tags
  150 + assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list.names
  151 + posts(:jonathan_sky).update_attributes!(:tag_list => "#{posts(:jonathan_sky).tag_list}, One, Two")
  152 + assert_equivalent ["Very good", "Nature", "One", "Two"], posts(:jonathan_sky).tag_list.names
  153 + end
  154 +
  155 + def test_remove_tag
  156 + assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list.names
  157 + posts(:jonathan_sky).update_attributes!(:tag_list => "Nature")
  158 + assert_equivalent ["Nature"], posts(:jonathan_sky).tag_list.names
  159 + end
  160 +
  161 + def test_change_case_of_tags
  162 + original_tag_names = photos(:jonathan_questioning_dog).tag_list.names
  163 + photos(:jonathan_questioning_dog).update_attributes!(:tag_list => photos(:jonathan_questioning_dog).tag_list.to_s.upcase)
  164 +
  165 + # The new tag list is not uppercase becuase the AR finders are not case-sensitive
  166 + # and find the old tags when re-tagging with the uppercase tags.
  167 + assert_equivalent original_tag_names, photos(:jonathan_questioning_dog).reload.tag_list.names
  168 + end
  169 +
  170 + def test_remove_and_add_tag
  171 + assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list.names
  172 + posts(:jonathan_sky).update_attributes!(:tag_list => "Nature, Beautiful")
  173 + assert_equivalent ["Nature", "Beautiful"], posts(:jonathan_sky).tag_list.names
  174 + end
  175 +
  176 + def test_tags_not_saved_if_validation_fails
  177 + assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list.names
  178 + assert !posts(:jonathan_sky).update_attributes(:tag_list => "One, Two", :text => "")
  179 + assert_equivalent ["Very good", "Nature"], Post.find(posts(:jonathan_sky).id).tag_list.names
  180 + end
  181 +
  182 + def test_tag_list_accessors_on_new_record
  183 + p = Post.new(:text => 'Test')
  184 +
  185 + assert p.tag_list.blank?
  186 + p.tag_list = "One, Two"
  187 + assert_equal "One, Two", p.tag_list.to_s
  188 + end
  189 +
  190 + def test_clear_tag_list_with_nil
  191 + p = photos(:jonathan_questioning_dog)
  192 +
  193 + assert !p.tag_list.blank?
  194 + assert p.update_attributes(:tag_list => nil)
  195 + assert p.tag_list.blank?
  196 +
  197 + assert p.reload.tag_list.blank?
  198 + end
  199 +
  200 + def test_clear_tag_list_with_string
  201 + p = photos(:jonathan_questioning_dog)
  202 +
  203 + assert !p.tag_list.blank?
  204 + assert p.update_attributes(:tag_list => ' ')
  205 + assert p.tag_list.blank?
  206 +
  207 + assert p.reload.tag_list.blank?
  208 + end
  209 +
  210 + def test_tag_list_reset_on_reload
  211 + p = photos(:jonathan_questioning_dog)
  212 + assert !p.tag_list.blank?
  213 + p.tag_list = nil
  214 + assert p.tag_list.blank?
  215 + assert !p.reload.tag_list.blank?
  216 + end
  217 +
  218 + def test_tag_list_populated_when_cache_nil
  219 + assert_nil posts(:jonathan_sky).cached_tag_list
  220 + posts(:jonathan_sky).save!
  221 + assert_equal posts(:jonathan_sky).tag_list.to_s, posts(:jonathan_sky).cached_tag_list
  222 + end
  223 +
  224 + def test_cached_tag_list_used
  225 + posts(:jonathan_sky).save!
  226 + posts(:jonathan_sky).reload
  227 +
  228 + assert_no_queries do
  229 + assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list.names
  230 + end
  231 + end
  232 +
  233 + def test_cached_tag_list_not_used
  234 + # Load fixture and column information
  235 + posts(:jonathan_sky).taggings(:reload)
  236 +
  237 + assert_queries 1 do
  238 + # Tags association will be loaded
  239 + posts(:jonathan_sky).tag_list
  240 + end
  241 + end
  242 +
  243 + def test_cached_tag_list_updated
  244 + assert_nil posts(:jonathan_sky).cached_tag_list
  245 + posts(:jonathan_sky).save!
  246 + assert_equivalent ["Very good", "Nature"], TagList.from(posts(:jonathan_sky).cached_tag_list).names
  247 + posts(:jonathan_sky).update_attributes!(:tag_list => "None")
  248 +
  249 + assert_equal 'None', posts(:jonathan_sky).cached_tag_list
  250 + assert_equal 'None', posts(:jonathan_sky).reload.cached_tag_list
  251 + end
  252 +
  253 + def test_inherited_taggable
  254 + # SpPost inherits acts_as_taggable from its ancestor Post
  255 + p = SpPost.new(:text => 'bla bla bla ...')
  256 + p.tag_list = 'bla'
  257 + p.save
  258 + assert !SpPost.find_tagged_with('bla').blank?
  259 + end
  260 +end
  261 +
  262 +class ActsAsTaggableOnSteroidsFormTest < Test::Unit::TestCase
  263 + fixtures :tags, :taggings, :posts, :users, :photos
  264 +
  265 + include ActionView::Helpers::FormHelper
  266 +
  267 + def test_tag_list_contents
  268 + fields_for :post, posts(:jonathan_sky) do |f|
  269 + assert_match /Very good, Nature/, f.text_field(:tag_list)
  270 + end
  271 + end
  272 +end
vendor/plugins/acts_as_taggable_on_steroids/test/database.yml 0 → 100644
@@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
  1 +mysql:
  2 + adapter: mysql
  3 + host: localhost
  4 + username: rails
  5 + password:
  6 + database: rails_plugin_test
  7 +
  8 +sqlite3:
  9 + adapter: sqlite3
  10 + database: ':memory:'
vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photo.rb 0 → 100644
@@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
  1 +class Photo < ActiveRecord::Base
  2 + acts_as_taggable
  3 +
  4 + belongs_to :user
  5 +end
  6 +
  7 +class SpecialPhoto < Photo
  8 +end
vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photos.yml 0 → 100644
@@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
  1 +jonathan_dog:
  2 + id: 1
  3 + user_id: 1
  4 + title: A small dog
  5 +
  6 +jonathan_questioning_dog:
  7 + id: 2
  8 + user_id: 1
  9 + title: What does this dog want?
  10 +
  11 +jonathan_bad_cat:
  12 + id: 3
  13 + user_id: 1
  14 + title: Bad cat
  15 +
  16 +sam_flower:
  17 + id: 4
  18 + user_id: 2
  19 + title: Flower
  20 +
  21 +sam_sky:
  22 + id: 5
  23 + user_id: 2
  24 + title: Sky
vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/post.rb 0 → 100644
@@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
  1 +class Post < ActiveRecord::Base
  2 + acts_as_taggable
  3 +
  4 + belongs_to :user
  5 +
  6 + validates_presence_of :text
  7 +end
vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/posts.yml 0 → 100644
@@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
  1 +jonathan_sky:
  2 + id: 1
  3 + user_id: 1
  4 + text: The sky is particularly blue today
  5 +
  6 +jonathan_grass:
  7 + id: 2
  8 + user_id: 1
  9 + text: The grass seems very green
  10 +
  11 +jonathan_rain:
  12 + id: 3
  13 + user_id: 1
  14 + text: Why does the rain fall?
  15 +
  16 +sam_ground:
  17 + id: 4
  18 + user_id: 2
  19 + text: The ground is looking too brown
  20 +
  21 +sam_flowers:
  22 + id: 5
  23 + user_id: 2
  24 + text: Why are the flowers dead?
0 \ No newline at end of file 25 \ No newline at end of file
vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/sp_post.rb 0 → 100644
@@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
  1 +class SpPost < Post
  2 + def ihnerited
  3 + true
  4 + end
  5 +end
vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/taggings.yml 0 → 100644
@@ -0,0 +1,126 @@ @@ -0,0 +1,126 @@
  1 +jonathan_sky_good:
  2 + id: 1
  3 + tag_id: 1
  4 + taggable_id: 1
  5 + taggable_type: Post
  6 + created_at: 2006-08-01
  7 +
  8 +jonathan_sky_nature:
  9 + id: 2
  10 + tag_id: 3
  11 + taggable_id: 1
  12 + taggable_type: Post
  13 + created_at: 2006-08-02
  14 +
  15 +jonathan_grass_nature:
  16 + id: 3
  17 + tag_id: 3
  18 + taggable_id: 2
  19 + taggable_type: Post
  20 + created_at: 2006-08-03
  21 +
  22 +jonathan_rain_question:
  23 + id: 4
  24 + tag_id: 4
  25 + taggable_id: 3
  26 + taggable_type: Post
  27 + created_at: 2006-08-04
  28 +
  29 +jonathan_rain_nature:
  30 + id: 5
  31 + tag_id: 3
  32 + taggable_id: 3
  33 + taggable_type: Post
  34 + created_at: 2006-08-05
  35 +
  36 +sam_ground_nature:
  37 + id: 6
  38 + tag_id: 3
  39 + taggable_id: 4
  40 + taggable_type: Post
  41 + created_at: 2006-08-06
  42 +
  43 +sam_ground_bad:
  44 + id: 7
  45 + tag_id: 2
  46 + taggable_id: 4
  47 + taggable_type: Post
  48 + created_at: 2006-08-07
  49 +
  50 +sam_flowers_good:
  51 + id: 8
  52 + tag_id: 1
  53 + taggable_id: 5
  54 + taggable_type: Post
  55 + created_at: 2006-08-08
  56 +
  57 +sam_flowers_nature:
  58 + id: 9
  59 + tag_id: 3
  60 + taggable_id: 5
  61 + taggable_type: Post
  62 + created_at: 2006-08-09
  63 +
  64 +
  65 +jonathan_dog_animal:
  66 + id: 10
  67 + tag_id: 5
  68 + taggable_id: 1
  69 + taggable_type: Photo
  70 + created_at: 2006-08-10
  71 +
  72 +jonathan_dog_nature:
  73 + id: 11
  74 + tag_id: 3
  75 + taggable_id: 1
  76 + taggable_type: Photo
  77 + created_at: 2006-08-11
  78 +
  79 +jonathan_questioning_dog_animal:
  80 + id: 12
  81 + tag_id: 5
  82 + taggable_id: 2
  83 + taggable_type: Photo
  84 + created_at: 2006-08-12
  85 +
  86 +jonathan_questioning_dog_question:
  87 + id: 13
  88 + tag_id: 4
  89 + taggable_id: 2
  90 + taggable_type: Photo
  91 + created_at: 2006-08-13
  92 +
  93 +jonathan_bad_cat_bad:
  94 + id: 14
  95 + tag_id: 2
  96 + taggable_id: 3
  97 + taggable_type: Photo
  98 + created_at: 2006-08-14
  99 +
  100 +jonathan_bad_cat_animal:
  101 + id: 15
  102 + tag_id: 5
  103 + taggable_id: 3
  104 + taggable_type: Photo
  105 + created_at: 2006-08-15
  106 +
  107 +sam_flower_nature:
  108 + id: 16
  109 + tag_id: 3
  110 + taggable_id: 4
  111 + taggable_type: Photo
  112 + created_at: 2006-08-16
  113 +
  114 +sam_flower_good:
  115 + id: 17
  116 + tag_id: 1
  117 + taggable_id: 4
  118 + taggable_type: Photo
  119 + created_at: 2006-08-17
  120 +
  121 +sam_sky_nature:
  122 + id: 18
  123 + tag_id: 3
  124 + taggable_id: 5
  125 + taggable_type: Photo
  126 + created_at: 2006-08-18
vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/tags.yml 0 → 100644
@@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
  1 +good:
  2 + id: 1
  3 + name: Very good
  4 +
  5 +bad:
  6 + id: 2
  7 + name: Bad
  8 +
  9 +nature:
  10 + id: 3
  11 + name: Nature
  12 +
  13 +question:
  14 + id: 4
  15 + name: Question
  16 +
  17 +animal:
  18 + id: 5
  19 + name: Crazy animal
0 \ No newline at end of file 20 \ No newline at end of file
vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/user.rb 0 → 100644
@@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
  1 +class User < ActiveRecord::Base
  2 + has_many :posts
  3 + has_many :photos
  4 +end
vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/users.yml 0 → 100644
@@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
  1 +jonathan:
  2 + id: 1
  3 + name: Jonathan
  4 +
  5 +sam:
  6 + id: 2
  7 + name: Sam
vendor/plugins/acts_as_taggable_on_steroids/test/schema.rb 0 → 100644
@@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
  1 +ActiveRecord::Schema.define :version => 0 do
  2 + create_table :tags, :force => true do |t|
  3 + t.column :name, :string
  4 + t.column :parent_id, :integer
  5 + t.column :pending, :boolean
  6 + end
  7 +
  8 + create_table :taggings, :force => true do |t|
  9 + t.column :tag_id, :integer
  10 + t.column :taggable_id, :integer
  11 + t.column :taggable_type, :string
  12 + t.column :created_at, :datetime
  13 + end
  14 +
  15 + create_table :users, :force => true do |t|
  16 + t.column :name, :string
  17 + end
  18 +
  19 + create_table :posts, :force => true do |t|
  20 + t.column :text, :text
  21 + t.column :cached_tag_list, :string
  22 + t.column :user_id, :integer
  23 +
  24 + t.column :type, :string
  25 + end
  26 +
  27 + create_table :photos, :force => true do |t|
  28 + t.column :title, :string
  29 + t.column :user_id, :integer
  30 + end
  31 +end
vendor/plugins/acts_as_taggable_on_steroids/test/tag_list_test.rb 0 → 100644
@@ -0,0 +1,98 @@ @@ -0,0 +1,98 @@
  1 +require File.dirname(__FILE__) + '/abstract_unit'
  2 +
  3 +class TagListTest < Test::Unit::TestCase
  4 + def test_blank?
  5 + assert TagList.new.blank?
  6 + end
  7 +
  8 + def test_equality
  9 + assert_equal TagList.new, TagList.new
  10 + assert_equal TagList.new("tag"), TagList.new("tag")
  11 +
  12 + assert_not_equal TagList.new, ""
  13 + assert_not_equal TagList.new, TagList.new("tag")
  14 + end
  15 +
  16 + def test_parse_leaves_string_unchanged
  17 + tags = '"one ", two'
  18 + original = tags.dup
  19 + TagList.parse(tags)
  20 + assert_equal tags, original
  21 + end
  22 +
  23 + def test_from_single_name
  24 + assert_equal %w(fun), TagList.from("fun").names
  25 + assert_equal %w(fun), TagList.from('"fun"').names
  26 + end
  27 +
  28 + def test_from_blank
  29 + assert_equal [], TagList.from(nil).names
  30 + assert_equal [], TagList.from("").names
  31 + end
  32 +
  33 + def test_from_single_quoted_tag
  34 + assert_equal ['with, comma'], TagList.from('"with, comma"').names
  35 + end
  36 +
  37 + def test_spaces_do_not_delineate
  38 + assert_equal ['a b', 'c'], TagList.from('a b, c').names
  39 + end
  40 +
  41 + def test_from_multiple_tags
  42 + assert_equivalent %w(alpha beta delta gamma), TagList.from("alpha, beta, delta, gamma").names.sort
  43 + end
  44 +
  45 + def test_from_multiple_tags_with_quotes
  46 + assert_equivalent %w(alpha beta delta gamma), TagList.from('alpha, "beta", gamma , "delta"').names.sort
  47 + end
  48 +
  49 + def test_from_multiple_tags_with_quote_and_commas
  50 + assert_equivalent ['alpha, beta', 'delta', 'gamma, something'], TagList.from('"alpha, beta", delta, "gamma, something"').names
  51 + end
  52 +
  53 + def test_from_removes_white_space
  54 + assert_equivalent %w(alpha beta), TagList.from('" alpha ", "beta "').names
  55 + assert_equivalent %w(alpha beta), TagList.from(' alpha, beta ').names
  56 + end
  57 +
  58 + def test_alternative_delimiter
  59 + TagList.delimiter = " "
  60 +
  61 + assert_equal %w(one two), TagList.from("one two").names
  62 + assert_equal ['one two', 'three', 'four'], TagList.from('"one two" three four').names
  63 + ensure
  64 + TagList.delimiter = ","
  65 + end
  66 +
  67 + def test_duplicate_tags_removed
  68 + assert_equal %w(one), TagList.from("one, one").names
  69 + end
  70 +
  71 + def test_to_s_with_commas
  72 + assert_equal "question, crazy animal", TagList.new(["question", "crazy animal"]).to_s
  73 + end
  74 +
  75 + def test_to_s_with_alternative_delimiter
  76 + TagList.delimiter = " "
  77 +
  78 + assert_equal '"crazy animal" question', TagList.new(["crazy animal", "question"]).to_s
  79 + ensure
  80 + TagList.delimiter = ","
  81 + end
  82 +
  83 + def test_add
  84 + tag_list = TagList.new("one")
  85 + assert_equal %w(one), tag_list.names
  86 +
  87 + tag_list.add("two")
  88 + assert_equal %w(one two), tag_list.names
  89 + end
  90 +
  91 + def test_remove
  92 + tag_list = TagList.new("one", "two")
  93 + assert_equal %w(one two), tag_list.names
  94 +
  95 + tag_list.remove("one")
  96 + assert_equal %w(two), tag_list.names
  97 + end
  98 +end
vendor/plugins/acts_as_taggable_on_steroids/test/tag_test.rb 0 → 100644
@@ -0,0 +1,42 @@ @@ -0,0 +1,42 @@
  1 +require File.dirname(__FILE__) + '/abstract_unit'
  2 +
  3 +class TagTest < Test::Unit::TestCase
  4 + fixtures :tags, :taggings, :users, :photos, :posts
  5 +
  6 + def test_name_required
  7 + t = Tag.create
  8 + assert_match /blank/, t.errors[:name].to_s
  9 + end
  10 +
  11 + def test_name_unique
  12 + t = Tag.create!(:name => "My tag")
  13 + duplicate = t.clone
  14 +
  15 + assert !duplicate.save
  16 + assert_match /taken/, duplicate.errors[:name].to_s
  17 + end
  18 +
  19 + def test_taggings
  20 + assert_equivalent [taggings(:jonathan_sky_good), taggings(:sam_flowers_good), taggings(:sam_flower_good)], tags(:good).taggings
  21 + assert_equivalent [taggings(:sam_ground_bad), taggings(:jonathan_bad_cat_bad)], tags(:bad).taggings
  22 + end
  23 +
  24 + def test_to_s
  25 + assert_equal tags(:good).name, tags(:good).to_s
  26 + end
  27 +
  28 + def test_equality
  29 + assert_equal tags(:good), tags(:good)
  30 + assert_equal Tag.find(1), Tag.find(1)
  31 + assert_equal Tag.new(:name => 'A'), Tag.new(:name => 'A')
  32 + assert_not_equal Tag.new(:name => 'A'), Tag.new(:name => 'B')
  33 + end
  34 +
  35 + def test_deprecated_delimiter
  36 + original_delimiter = Tag.delimiter
  37 + Tag.delimiter = ":"
  38 + assert_equal ":", TagList.delimiter
  39 + ensure
  40 + TagList.delimiter = original_delimiter
  41 + end
  42 +end
vendor/plugins/acts_as_taggable_on_steroids/test/tagging_test.rb 0 → 100644
@@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
  1 +require File.dirname(__FILE__) + '/abstract_unit'
  2 +
  3 +class TaggingTest < Test::Unit::TestCase
  4 + fixtures :tags, :taggings, :posts
  5 +
  6 + def test_tag
  7 + assert_equal tags(:good), taggings(:jonathan_sky_good).tag
  8 + end
  9 +
  10 + def test_taggable
  11 + assert_equal posts(:jonathan_sky), taggings(:jonathan_sky_good).taggable
  12 + end
  13 +end
vendor/plugins/acts_as_versioned/CHANGELOG 0 → 100644
@@ -0,0 +1,74 @@ @@ -0,0 +1,74 @@
  1 +*SVN* (version numbers are overrated)
  2 +
  3 +* (5 Oct 2006) Allow customization of #versions association options [Dan Peterson]
  4 +
  5 +*0.5.1*
  6 +
  7 +* (8 Aug 2006) Versioned models now belong to the unversioned model. @article_version.article.class => Article [Aslak Hellesoy]
  8 +
  9 +*0.5* # do versions even matter for plugins?
  10 +
  11 +* (21 Apr 2006) Added without_locking and without_revision methods.
  12 +
  13 + Foo.without_revision do
  14 + @foo.update_attributes ...
  15 + end
  16 +
  17 +*0.4*
  18 +
  19 +* (28 March 2006) Rename non_versioned_fields to non_versioned_columns (old one is kept for compatibility).
  20 +* (28 March 2006) Made explicit documentation note that string column names are required for non_versioned_columns.
  21 +
  22 +*0.3.1*
  23 +
  24 +* (7 Jan 2006) explicitly set :foreign_key option for the versioned model's belongs_to assocation for STI [Caged]
  25 +* (7 Jan 2006) added tests to prove has_many :through joins work
  26 +
  27 +*0.3*
  28 +
  29 +* (2 Jan 2006) added ability to share a mixin with versioned class
  30 +* (2 Jan 2006) changed the dynamic version model to MyModel::Version
  31 +
  32 +*0.2.4*
  33 +
  34 +* (27 Nov 2005) added note about possible destructive behavior of if_changed? [Michael Schuerig]
  35 +
  36 +*0.2.3*
  37 +
  38 +* (12 Nov 2005) fixed bug with old behavior of #blank? [Michael Schuerig]
  39 +* (12 Nov 2005) updated tests to use ActiveRecord Schema
  40 +
  41 +*0.2.2*
  42 +
  43 +* (3 Nov 2005) added documentation note to #acts_as_versioned [Martin Jul]
  44 +
  45 +*0.2.1*
  46 +
  47 +* (6 Oct 2005) renamed dirty? to changed? to keep it uniform. it was aliased to keep it backwards compatible.
  48 +
  49 +*0.2*
  50 +
  51 +* (6 Oct 2005) added find_versions and find_version class methods.
  52 +
  53 +* (6 Oct 2005) removed transaction from create_versioned_table().
  54 + this way you can specify your own transaction around a group of operations.
  55 +
  56 +* (30 Sep 2005) fixed bug where find_versions() would order by 'version' twice. (found by Joe Clark)
  57 +
  58 +* (26 Sep 2005) added :sequence_name option to acts_as_versioned to set the sequence name on the versioned model
  59 +
  60 +*0.1.3* (18 Sep 2005)
  61 +
  62 +* First RubyForge release
  63 +
  64 +*0.1.2*
  65 +
  66 +* check if module is already included when acts_as_versioned is called
  67 +
  68 +*0.1.1*
  69 +
  70 +* Adding tests and rdocs
  71 +
  72 +*0.1*
  73 +
  74 +* Initial transfer from Rails ticket: http://dev.rubyonrails.com/ticket/1974
0 \ No newline at end of file 75 \ No newline at end of file
vendor/plugins/acts_as_versioned/MIT-LICENSE 0 → 100644
@@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
  1 +Copyright (c) 2005 Rick Olson
  2 +
  3 +Permission is hereby granted, free of charge, to any person obtaining
  4 +a copy of this software and associated documentation files (the
  5 +"Software"), to deal in the Software without restriction, including
  6 +without limitation the rights to use, copy, modify, merge, publish,
  7 +distribute, sublicense, and/or sell copies of the Software, and to
  8 +permit persons to whom the Software is furnished to do so, subject to
  9 +the following conditions:
  10 +
  11 +The above copyright notice and this permission notice shall be
  12 +included in all copies or substantial portions of the Software.
  13 +
  14 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  15 +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  16 +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  17 +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  18 +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  19 +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  20 +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
0 \ No newline at end of file 21 \ No newline at end of file
vendor/plugins/acts_as_versioned/README 0 → 100644
@@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
  1 += acts_as_versioned
  2 +
  3 +This library adds simple versioning to an ActiveRecord module. ActiveRecord is required.
  4 +
  5 +== Resources
  6 +
  7 +Install
  8 +
  9 +* gem install acts_as_versioned
  10 +
  11 +Rubyforge project
  12 +
  13 +* http://rubyforge.org/projects/ar-versioned
  14 +
  15 +RDocs
  16 +
  17 +* http://ar-versioned.rubyforge.org
  18 +
  19 +Subversion
  20 +
  21 +* http://techno-weenie.net/svn/projects/acts_as_versioned
  22 +
  23 +Collaboa
  24 +
  25 +* http://collaboa.techno-weenie.net/repository/browse/acts_as_versioned
  26 +
  27 +Special thanks to Dreamer on ##rubyonrails for help in early testing. His ServerSideWiki (http://serversidewiki.com)
  28 +was the first project to use acts_as_versioned <em>in the wild</em>.
0 \ No newline at end of file 29 \ No newline at end of file
vendor/plugins/acts_as_versioned/RUNNING_UNIT_TESTS 0 → 100644
@@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
  1 +== Creating the test database
  2 +
  3 +The default name for the test databases is "activerecord_versioned". If you
  4 +want to use another database name then be sure to update the connection
  5 +adapter setups you want to test with in test/connections/<your database>/connection.rb.
  6 +When you have the database online, you can import the fixture tables with
  7 +the test/fixtures/db_definitions/*.sql files.
  8 +
  9 +Make sure that you create database objects with the same user that you specified in i
  10 +connection.rb otherwise (on Postgres, at least) tests for default values will fail.
  11 +
  12 +== Running with Rake
  13 +
  14 +The easiest way to run the unit tests is through Rake. The default task runs
  15 +the entire test suite for all the adapters. You can also run the suite on just
  16 +one adapter by using the tasks test_mysql_ruby, test_ruby_mysql, test_sqlite,
  17 +or test_postresql. For more information, checkout the full array of rake tasks with "rake -T"
  18 +
  19 +Rake can be found at http://rake.rubyforge.org
  20 +
  21 +== Running by hand
  22 +
  23 +Unit tests are located in test directory. If you only want to run a single test suite,
  24 +or don't want to bother with Rake, you can do so with something like:
  25 +
  26 + cd test; ruby -I "connections/native_mysql" base_test.rb
  27 +
  28 +That'll run the base suite using the MySQL-Ruby adapter. Change the adapter
  29 +and test suite name as needed.
  30 +
  31 +== Faster tests
  32 +
  33 +If you are using a database that supports transactions, you can set the
  34 +"AR_TX_FIXTURES" environment variable to "yes" to use transactional fixtures.
  35 +This gives a very large speed boost. With rake:
  36 +
  37 + rake AR_TX_FIXTURES=yes
  38 +
  39 +Or, by hand:
  40 +
  41 + AR_TX_FIXTURES=yes ruby -I connections/native_sqlite3 base_test.rb
vendor/plugins/acts_as_versioned/Rakefile 0 → 100644
@@ -0,0 +1,182 @@ @@ -0,0 +1,182 @@
  1 +require 'rubygems'
  2 +
  3 +Gem::manage_gems
  4 +
  5 +require 'rake/rdoctask'
  6 +require 'rake/packagetask'
  7 +require 'rake/gempackagetask'
  8 +require 'rake/testtask'
  9 +require 'rake/contrib/rubyforgepublisher'
  10 +
  11 +PKG_NAME = 'acts_as_versioned'
  12 +PKG_VERSION = '0.3.1'
  13 +PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
  14 +PROD_HOST = "technoweenie@bidwell.textdrive.com"
  15 +RUBY_FORGE_PROJECT = 'ar-versioned'
  16 +RUBY_FORGE_USER = 'technoweenie'
  17 +
  18 +desc 'Default: run unit tests.'
  19 +task :default => :test
  20 +
  21 +desc 'Test the calculations plugin.'
  22 +Rake::TestTask.new(:test) do |t|
  23 + t.libs << 'lib'
  24 + t.pattern = 'test/**/*_test.rb'
  25 + t.verbose = true
  26 +end
  27 +
  28 +desc 'Generate documentation for the calculations plugin.'
  29 +Rake::RDocTask.new(:rdoc) do |rdoc|
  30 + rdoc.rdoc_dir = 'rdoc'
  31 + rdoc.title = "#{PKG_NAME} -- Simple versioning with active record models"
  32 + rdoc.options << '--line-numbers --inline-source'
  33 + rdoc.rdoc_files.include('README', 'CHANGELOG', 'RUNNING_UNIT_TESTS')
  34 + rdoc.rdoc_files.include('lib/**/*.rb')
  35 +end
  36 +
  37 +spec = Gem::Specification.new do |s|
  38 + s.name = PKG_NAME
  39 + s.version = PKG_VERSION
  40 + s.platform = Gem::Platform::RUBY
  41 + s.summary = "Simple versioning with active record models"
  42 + s.files = FileList["{lib,test}/**/*"].to_a + %w(README MIT-LICENSE CHANGELOG RUNNING_UNIT_TESTS)
  43 + s.files.delete "acts_as_versioned_plugin.sqlite.db"
  44 + s.files.delete "acts_as_versioned_plugin.sqlite3.db"
  45 + s.files.delete "test/debug.log"
  46 + s.require_path = 'lib'
  47 + s.autorequire = 'acts_as_versioned'
  48 + s.has_rdoc = true
  49 + s.test_files = Dir['test/**/*_test.rb']
  50 + s.add_dependency 'activerecord', '>= 1.10.1'
  51 + s.add_dependency 'activesupport', '>= 1.1.1'
  52 + s.author = "Rick Olson"
  53 + s.email = "technoweenie@gmail.com"
  54 + s.homepage = "http://techno-weenie.net"
  55 +end
  56 +
  57 +Rake::GemPackageTask.new(spec) do |pkg|
  58 + pkg.need_tar = true
  59 +end
  60 +
  61 +desc "Publish the API documentation"
  62 +task :pdoc => [:rdoc] do
  63 + Rake::RubyForgePublisher.new(RUBY_FORGE_PROJECT, RUBY_FORGE_USER).upload
  64 +end
  65 +
  66 +desc 'Publish the gem and API docs'
  67 +task :publish => [:pdoc, :rubyforge_upload]
  68 +
  69 +desc "Publish the release files to RubyForge."
  70 +task :rubyforge_upload => :package do
  71 + files = %w(gem tgz).map { |ext| "pkg/#{PKG_FILE_NAME}.#{ext}" }
  72 +
  73 + if RUBY_FORGE_PROJECT then
  74 + require 'net/http'
  75 + require 'open-uri'
  76 +
  77 + project_uri = "http://rubyforge.org/projects/#{RUBY_FORGE_PROJECT}/"
  78 + project_data = open(project_uri) { |data| data.read }
  79 + group_id = project_data[/[?&]group_id=(\d+)/, 1]
  80 + raise "Couldn't get group id" unless group_id
  81 +
  82 + # This echos password to shell which is a bit sucky
  83 + if ENV["RUBY_FORGE_PASSWORD"]
  84 + password = ENV["RUBY_FORGE_PASSWORD"]
  85 + else
  86 + print "#{RUBY_FORGE_USER}@rubyforge.org's password: "
  87 + password = STDIN.gets.chomp
  88 + end
  89 +
  90 + login_response = Net::HTTP.start("rubyforge.org", 80) do |http|
  91 + data = [
  92 + "login=1",
  93 + "form_loginname=#{RUBY_FORGE_USER}",
  94 + "form_pw=#{password}"
  95 + ].join("&")
  96 + http.post("/account/login.php", data)
  97 + end
  98 +
  99 + cookie = login_response["set-cookie"]
  100 + raise "Login failed" unless cookie
  101 + headers = { "Cookie" => cookie }
  102 +
  103 + release_uri = "http://rubyforge.org/frs/admin/?group_id=#{group_id}"
  104 + release_data = open(release_uri, headers) { |data| data.read }
  105 + package_id = release_data[/[?&]package_id=(\d+)/, 1]
  106 + raise "Couldn't get package id" unless package_id
  107 +
  108 + first_file = true
  109 + release_id = ""
  110 +
  111 + files.each do |filename|
  112 + basename = File.basename(filename)
  113 + file_ext = File.extname(filename)
  114 + file_data = File.open(filename, "rb") { |file| file.read }
  115 +
  116 + puts "Releasing #{basename}..."
  117 +
  118 + release_response = Net::HTTP.start("rubyforge.org", 80) do |http|
  119 + release_date = Time.now.strftime("%Y-%m-%d %H:%M")
  120 + type_map = {
  121 + ".zip" => "3000",
  122 + ".tgz" => "3110",
  123 + ".gz" => "3110",
  124 + ".gem" => "1400"
  125 + }; type_map.default = "9999"
  126 + type = type_map[file_ext]
  127 + boundary = "rubyqMY6QN9bp6e4kS21H4y0zxcvoor"
  128 +
  129 + query_hash = if first_file then
  130 + {
  131 + "group_id" => group_id,
  132 + "package_id" => package_id,
  133 + "release_name" => PKG_FILE_NAME,
  134 + "release_date" => release_date,
  135 + "type_id" => type,
  136 + "processor_id" => "8000", # Any
  137 + "release_notes" => "",
  138 + "release_changes" => "",
  139 + "preformatted" => "1",
  140 + "submit" => "1"
  141 + }
  142 + else
  143 + {
  144 + "group_id" => group_id,
  145 + "release_id" => release_id,
  146 + "package_id" => package_id,
  147 + "step2" => "1",
  148 + "type_id" => type,
  149 + "processor_id" => "8000", # Any
  150 + "submit" => "Add This File"
  151 + }
  152 + end
  153 +
  154 + query = "?" + query_hash.map do |(name, value)|
  155 + [name, URI.encode(value)].join("=")
  156 + end.join("&")
  157 +
  158 + data = [
  159 + "--" + boundary,
  160 + "Content-Disposition: form-data; name=\"userfile\"; filename=\"#{basename}\"",
  161 + "Content-Type: application/octet-stream",
  162 + "Content-Transfer-Encoding: binary",
  163 + "", file_data, ""
  164 + ].join("\x0D\x0A")
  165 +
  166 + release_headers = headers.merge(
  167 + "Content-Type" => "multipart/form-data; boundary=#{boundary}"
  168 + )
  169 +
  170 + target = first_file ? "/frs/admin/qrs.php" : "/frs/admin/editrelease.php"
  171 + http.post(target + query, data, release_headers)
  172 + end
  173 +
  174 + if first_file then
  175 + release_id = release_response.body[/release_id=(\d+)/, 1]
  176 + raise("Couldn't get release id") unless release_id
  177 + end
  178 +
  179 + first_file = false
  180 + end
  181 + end
  182 +end
0 \ No newline at end of file 183 \ No newline at end of file
vendor/plugins/acts_as_versioned/init.rb 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +require 'acts_as_versioned'
0 \ No newline at end of file 2 \ No newline at end of file
vendor/plugins/acts_as_versioned/lib/acts_as_versioned.rb 0 → 100644
@@ -0,0 +1,545 @@ @@ -0,0 +1,545 @@
  1 +# Copyright (c) 2005 Rick Olson
  2 +#
  3 +# Permission is hereby granted, free of charge, to any person obtaining
  4 +# a copy of this software and associated documentation files (the
  5 +# "Software"), to deal in the Software without restriction, including
  6 +# without limitation the rights to use, copy, modify, merge, publish,
  7 +# distribute, sublicense, and/or sell copies of the Software, and to
  8 +# permit persons to whom the Software is furnished to do so, subject to
  9 +# the following conditions:
  10 +#
  11 +# The above copyright notice and this permission notice shall be
  12 +# included in all copies or substantial portions of the Software.
  13 +#
  14 +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  15 +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  16 +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  17 +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  18 +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  19 +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  20 +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  21 +
  22 +module ActiveRecord #:nodoc:
  23 + module Acts #:nodoc:
  24 + # Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a
  25 + # versioned table ready and that your model has a version field. This works with optimistic locking if the lock_version
  26 + # column is present as well.
  27 + #
  28 + # The class for the versioned model is derived the first time it is seen. Therefore, if you change your database schema you have to restart
  29 + # your container for the changes to be reflected. In development mode this usually means restarting WEBrick.
  30 + #
  31 + # class Page < ActiveRecord::Base
  32 + # # assumes pages_versions table
  33 + # acts_as_versioned
  34 + # end
  35 + #
  36 + # Example:
  37 + #
  38 + # page = Page.create(:title => 'hello world!')
  39 + # page.version # => 1
  40 + #
  41 + # page.title = 'hello world'
  42 + # page.save
  43 + # page.version # => 2
  44 + # page.versions.size # => 2
  45 + #
  46 + # page.revert_to(1) # using version number
  47 + # page.title # => 'hello world!'
  48 + #
  49 + # page.revert_to(page.versions.last) # using versioned instance
  50 + # page.title # => 'hello world'
  51 + #
  52 + # page.versions.earliest # efficient query to find the first version
  53 + # page.versions.latest # efficient query to find the most recently created version
  54 + #
  55 + #
  56 + # Simple Queries to page between versions
  57 + #
  58 + # page.versions.before(version)
  59 + # page.versions.after(version)
  60 + #
  61 + # Access the previous/next versions from the versioned model itself
  62 + #
  63 + # version = page.versions.latest
  64 + # version.previous # go back one version
  65 + # version.next # go forward one version
  66 + #
  67 + # See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
  68 + module Versioned
  69 + CALLBACKS = [:set_new_version, :save_version_on_create, :save_version?, :clear_changed_attributes]
  70 + def self.included(base) # :nodoc:
  71 + base.extend ClassMethods
  72 + end
  73 +
  74 + module ClassMethods
  75 + # == Configuration options
  76 + #
  77 + # * <tt>class_name</tt> - versioned model class name (default: PageVersion in the above example)
  78 + # * <tt>table_name</tt> - versioned model table name (default: page_versions in the above example)
  79 + # * <tt>foreign_key</tt> - foreign key used to relate the versioned model to the original model (default: page_id in the above example)
  80 + # * <tt>inheritance_column</tt> - name of the column to save the model's inheritance_column value for STI. (default: versioned_type)
  81 + # * <tt>version_column</tt> - name of the column in the model that keeps the version number (default: version)
  82 + # * <tt>sequence_name</tt> - name of the custom sequence to be used by the versioned model.
  83 + # * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
  84 + # * <tt>if</tt> - symbol of method to check before saving a new version. If this method returns false, a new version is not saved.
  85 + # For finer control, pass either a Proc or modify Model#version_condition_met?
  86 + #
  87 + # acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
  88 + #
  89 + # or...
  90 + #
  91 + # class Auction
  92 + # def version_condition_met? # totally bypasses the <tt>:if</tt> option
  93 + # !expired?
  94 + # end
  95 + # end
  96 + #
  97 + # * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
  98 + # either a symbol or array of symbols. WARNING - This will attempt to overwrite any attribute setters you may have.
  99 + # Use this instead if you want to write your own attribute setters (and ignore if_changed):
  100 + #
  101 + # def name=(new_name)
  102 + # write_changed_attribute :name, new_name
  103 + # end
  104 + #
  105 + # * <tt>extend</tt> - Lets you specify a module to be mixed in both the original and versioned models. You can also just pass a block
  106 + # to create an anonymous mixin:
  107 + #
  108 + # class Auction
  109 + # acts_as_versioned do
  110 + # def started?
  111 + # !started_at.nil?
  112 + # end
  113 + # end
  114 + # end
  115 + #
  116 + # or...
  117 + #
  118 + # module AuctionExtension
  119 + # def started?
  120 + # !started_at.nil?
  121 + # end
  122 + # end
  123 + # class Auction
  124 + # acts_as_versioned :extend => AuctionExtension
  125 + # end
  126 + #
  127 + # Example code:
  128 + #
  129 + # @auction = Auction.find(1)
  130 + # @auction.started?
  131 + # @auction.versions.first.started?
  132 + #
  133 + # == Database Schema
  134 + #
  135 + # The model that you're versioning needs to have a 'version' attribute. The model is versioned
  136 + # into a table called #{model}_versions where the model name is singlular. The _versions table should
  137 + # contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
  138 + #
  139 + # A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
  140 + # then that field is reflected in the versioned model as 'versioned_type' by default.
  141 + #
  142 + # Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
  143 + # method, perfect for a migration. It will also create the version column if the main model does not already have it.
  144 + #
  145 + # class AddVersions < ActiveRecord::Migration
  146 + # def self.up
  147 + # # create_versioned_table takes the same options hash
  148 + # # that create_table does
  149 + # Post.create_versioned_table
  150 + # end
  151 + #
  152 + # def self.down
  153 + # Post.drop_versioned_table
  154 + # end
  155 + # end
  156 + #
  157 + # == Changing What Fields Are Versioned
  158 + #
  159 + # By default, acts_as_versioned will version all but these fields:
  160 + #
  161 + # [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
  162 + #
  163 + # You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols.
  164 + #
  165 + # class Post < ActiveRecord::Base
  166 + # acts_as_versioned
  167 + # self.non_versioned_columns << 'comments_count'
  168 + # end
  169 + #
  170 + def acts_as_versioned(options = {}, &extension)
  171 + # don't allow multiple calls
  172 + return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods)
  173 +
  174 + send :include, ActiveRecord::Acts::Versioned::ActMethods
  175 +
  176 + cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
  177 + :version_column, :max_version_limit, :track_changed_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
  178 + :version_association_options
  179 +
  180 + # legacy
  181 + alias_method :non_versioned_fields, :non_versioned_columns
  182 + alias_method :non_versioned_fields=, :non_versioned_columns=
  183 +
  184 + class << self
  185 + alias_method :non_versioned_fields, :non_versioned_columns
  186 + alias_method :non_versioned_fields=, :non_versioned_columns=
  187 + end
  188 +
  189 + send :attr_accessor, :changed_attributes
  190 +
  191 + self.versioned_class_name = options[:class_name] || "Version"
  192 + self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
  193 + self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
  194 + self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
  195 + self.version_column = options[:version_column] || 'version'
  196 + self.version_sequence_name = options[:sequence_name]
  197 + self.max_version_limit = options[:limit].to_i
  198 + self.version_condition = options[:if] || true
  199 + self.non_versioned_columns = [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
  200 + self.version_association_options = {
  201 + :class_name => "#{self.to_s}::#{versioned_class_name}",
  202 + :foreign_key => versioned_foreign_key,
  203 + :order => 'version',
  204 + :dependent => :delete_all
  205 + }.merge(options[:association_options] || {})
  206 +
  207 + if block_given?
  208 + extension_module_name = "#{versioned_class_name}Extension"
  209 + silence_warnings do
  210 + self.const_set(extension_module_name, Module.new(&extension))
  211 + end
  212 +
  213 + options[:extend] = self.const_get(extension_module_name)
  214 + end
  215 +
  216 + class_eval do
  217 + has_many :versions, version_association_options do
  218 + # finds earliest version of this record
  219 + def earliest
  220 + @earliest ||= find(:first)
  221 + end
  222 +
  223 + # find latest version of this record
  224 + def latest
  225 + @latest ||= find(:first, :order => 'version desc')
  226 + end
  227 + end
  228 + before_save :set_new_version
  229 + after_create :save_version_on_create
  230 + after_update :save_version
  231 + after_save :clear_old_versions
  232 + after_save :clear_changed_attributes
  233 +
  234 + unless options[:if_changed].nil?
  235 + self.track_changed_attributes = true
  236 + options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
  237 + options[:if_changed].each do |attr_name|
  238 + define_method("#{attr_name}=") do |value|
  239 + write_changed_attribute attr_name, value
  240 + end
  241 + end
  242 + end
  243 +
  244 + include options[:extend] if options[:extend].is_a?(Module)
  245 + end
  246 +
  247 + # create the dynamic versioned model
  248 + const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
  249 + def self.reloadable? ; false ; end
  250 + # find first version before the given version
  251 + def self.before(version)
  252 + find :first, :order => 'version desc',
  253 + :conditions => ["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version]
  254 + end
  255 +
  256 + # find first version after the given version.
  257 + def self.after(version)
  258 + find :first, :order => 'version',
  259 + :conditions => ["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version]
  260 + end
  261 +
  262 + def previous
  263 + self.class.before(self)
  264 + end
  265 +
  266 + def next
  267 + self.class.after(self)
  268 + end
  269 + end
  270 +
  271 + versioned_class.cattr_accessor :original_class
  272 + versioned_class.original_class = self
  273 + versioned_class.set_table_name versioned_table_name
  274 + versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
  275 + :class_name => "::#{self.to_s}",
  276 + :foreign_key => versioned_foreign_key
  277 + versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
  278 + versioned_class.set_sequence_name version_sequence_name if version_sequence_name
  279 + end
  280 + end
  281 +
  282 + module ActMethods
  283 + def self.included(base) # :nodoc:
  284 + base.extend ClassMethods
  285 + end
  286 +
  287 + # Saves a version of the model if applicable
  288 + def save_version
  289 + save_version_on_create if save_version?
  290 + end
  291 +
  292 + # Saves a version of the model in the versioned table. This is called in the after_save callback by default
  293 + def save_version_on_create
  294 + rev = self.class.versioned_class.new
  295 + self.clone_versioned_model(self, rev)
  296 + rev.version = send(self.class.version_column)
  297 + rev.send("#{self.class.versioned_foreign_key}=", self.id)
  298 + rev.save
  299 + end
  300 +
  301 + # Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
  302 + # Override this method to set your own criteria for clearing old versions.
  303 + def clear_old_versions
  304 + return if self.class.max_version_limit == 0
  305 + excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
  306 + if excess_baggage > 0
  307 + sql = "DELETE FROM #{self.class.versioned_table_name} WHERE version <= #{excess_baggage} AND #{self.class.versioned_foreign_key} = #{self.id}"
  308 + self.class.versioned_class.connection.execute sql
  309 + end
  310 + end
  311 +
  312 + # Reverts a model to a given version. Takes either a version number or an instance of the versioned model
  313 + def revert_to(version)
  314 + if version.is_a?(self.class.versioned_class)
  315 + return false unless version.send(self.class.versioned_foreign_key) == self.id and !version.new_record?
  316 + else
  317 + return false unless version = versions.find_by_version(version)
  318 + end
  319 + self.clone_versioned_model(version, self)
  320 + self.send("#{self.class.version_column}=", version.version)
  321 + true
  322 + end
  323 +
  324 + # Reverts a model to a given version and saves the model.
  325 + # Takes either a version number or an instance of the versioned model
  326 + def revert_to!(version)
  327 + revert_to(version) ? save_without_revision : false
  328 + end
  329 +
  330 + # Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
  331 + def save_without_revision
  332 + save_without_revision!
  333 + true
  334 + rescue
  335 + false
  336 + end
  337 +
  338 + def save_without_revision!
  339 + without_locking do
  340 + without_revision do
  341 + save!
  342 + end
  343 + end
  344 + end
  345 +
  346 + # Returns an array of attribute keys that are versioned. See non_versioned_columns
  347 + def versioned_attributes
  348 + self.attributes.keys.select { |k| !self.class.non_versioned_columns.include?(k) }
  349 + end
  350 +
  351 + # If called with no parameters, gets whether the current model has changed and needs to be versioned.
  352 + # If called with a single parameter, gets whether the parameter has changed.
  353 + def changed?(attr_name = nil)
  354 + attr_name.nil? ?
  355 + (!self.class.track_changed_attributes || (changed_attributes && changed_attributes.length > 0)) :
  356 + (changed_attributes && changed_attributes.include?(attr_name.to_s))
  357 + end
  358 +
  359 + # keep old dirty? method
  360 + alias_method :dirty?, :changed?
  361 +
  362 + # Clones a model. Used when saving a new version or reverting a model's version.
  363 + def clone_versioned_model(orig_model, new_model)
  364 + self.versioned_attributes.each do |key|
  365 + new_model.send("#{key}=", orig_model.send(key)) if orig_model.has_attribute?(key)
  366 + end
  367 +
  368 + if orig_model.is_a?(self.class.versioned_class)
  369 + new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
  370 + elsif new_model.is_a?(self.class.versioned_class)
  371 + new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
  372 + end
  373 + end
  374 +
  375 + # Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
  376 + def save_version?
  377 + version_condition_met? && changed?
  378 + end
  379 +
  380 + # Checks condition set in the :if option to check whether a revision should be created or not. Override this for
  381 + # custom version condition checking.
  382 + def version_condition_met?
  383 + case
  384 + when version_condition.is_a?(Symbol)
  385 + send(version_condition)
  386 + when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
  387 + version_condition.call(self)
  388 + else
  389 + version_condition
  390 + end
  391 + end
  392 +
  393 + # Executes the block with the versioning callbacks disabled.
  394 + #
  395 + # @foo.without_revision do
  396 + # @foo.save
  397 + # end
  398 + #
  399 + def without_revision(&block)
  400 + self.class.without_revision(&block)
  401 + end
  402 +
  403 + # Turns off optimistic locking for the duration of the block
  404 + #
  405 + # @foo.without_locking do
  406 + # @foo.save
  407 + # end
  408 + #
  409 + def without_locking(&block)
  410 + self.class.without_locking(&block)
  411 + end
  412 +
  413 + def empty_callback() end #:nodoc:
  414 +
  415 + protected
  416 + # sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
  417 + def set_new_version
  418 + self.send("#{self.class.version_column}=", self.next_version) if new_record? || (!locking_enabled? && save_version?)
  419 + end
  420 +
  421 + # Gets the next available version for the current record, or 1 for a new record
  422 + def next_version
  423 + return 1 if new_record?
  424 + (versions.calculate(:max, :version) || 0) + 1
  425 + end
  426 +
  427 + # clears current changed attributes. Called after save.
  428 + def clear_changed_attributes
  429 + self.changed_attributes = []
  430 + end
  431 +
  432 + def write_changed_attribute(attr_name, attr_value)
  433 + # Convert to db type for comparison. Avoids failing Float<=>String comparisons.
  434 + attr_value_for_db = self.class.columns_hash[attr_name.to_s].type_cast(attr_value)
  435 + (self.changed_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) || self.send(attr_name) == attr_value_for_db
  436 + write_attribute(attr_name, attr_value_for_db)
  437 + end
  438 +
  439 + module ClassMethods
  440 + # Finds a specific version of a specific row of this model
  441 + def find_version(id, version)
  442 + find_versions(id,
  443 + :conditions => ["#{versioned_foreign_key} = ? AND version = ?", id, version],
  444 + :limit => 1).first
  445 + end
  446 +
  447 + # Finds versions of a specific model. Takes an options hash like <tt>find</tt>
  448 + def find_versions(id, options = {})
  449 + versioned_class.find :all, {
  450 + :conditions => ["#{versioned_foreign_key} = ?", id],
  451 + :order => 'version' }.merge(options)
  452 + end
  453 +
  454 + # Returns an array of columns that are versioned. See non_versioned_columns
  455 + def versioned_columns
  456 + self.columns.select { |c| !non_versioned_columns.include?(c.name) }
  457 + end
  458 +
  459 + # Returns an instance of the dynamic versioned model
  460 + def versioned_class
  461 + const_get versioned_class_name
  462 + end
  463 +
  464 + # Rake migration task to create the versioned table using options passed to acts_as_versioned
  465 + def create_versioned_table(create_table_options = {})
  466 + # create version column in main table if it does not exist
  467 + if !self.content_columns.find { |c| %w(version lock_version).include? c.name }
  468 + self.connection.add_column table_name, :version, :integer
  469 + end
  470 +
  471 + self.connection.create_table(versioned_table_name, create_table_options) do |t|
  472 + t.column versioned_foreign_key, :integer
  473 + t.column :version, :integer
  474 + end
  475 +
  476 + updated_col = nil
  477 + self.versioned_columns.each do |col|
  478 + updated_col = col if !updated_col && %(updated_at updated_on).include?(col.name)
  479 + self.connection.add_column versioned_table_name, col.name, col.type,
  480 + :limit => col.limit,
  481 + :default => col.default,
  482 + :scale => col.scale,
  483 + :precision => col.precision
  484 + end
  485 +
  486 + if type_col = self.columns_hash[inheritance_column]
  487 + self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
  488 + :limit => type_col.limit,
  489 + :default => type_col.default,
  490 + :scale => type_col.scale,
  491 + :precision => type_col.precision
  492 + end
  493 +
  494 + if updated_col.nil?
  495 + self.connection.add_column versioned_table_name, :updated_at, :timestamp
  496 + end
  497 + end
  498 +
  499 + # Rake migration task to drop the versioned table
  500 + def drop_versioned_table
  501 + self.connection.drop_table versioned_table_name
  502 + end
  503 +
  504 + # Executes the block with the versioning callbacks disabled.
  505 + #
  506 + # Foo.without_revision do
  507 + # @foo.save
  508 + # end
  509 + #
  510 + def without_revision(&block)
  511 + class_eval do
  512 + CALLBACKS.each do |attr_name|
  513 + alias_method "orig_#{attr_name}".to_sym, attr_name
  514 + alias_method attr_name, :empty_callback
  515 + end
  516 + end
  517 + block.call
  518 + ensure
  519 + class_eval do
  520 + CALLBACKS.each do |attr_name|
  521 + alias_method attr_name, "orig_#{attr_name}".to_sym
  522 + end
  523 + end
  524 + end
  525 +
  526 + # Turns off optimistic locking for the duration of the block
  527 + #
  528 + # Foo.without_locking do
  529 + # @foo.save
  530 + # end
  531 + #
  532 + def without_locking(&block)
  533 + current = ActiveRecord::Base.lock_optimistically
  534 + ActiveRecord::Base.lock_optimistically = false if current
  535 + result = block.call
  536 + ActiveRecord::Base.lock_optimistically = true if current
  537 + result
  538 + end
  539 + end
  540 + end
  541 + end
  542 + end
  543 +end
  544 +
  545 +ActiveRecord::Base.send :include, ActiveRecord::Acts::Versioned
0 \ No newline at end of file 546 \ No newline at end of file
vendor/plugins/acts_as_versioned/test/abstract_unit.rb 0 → 100644
@@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
  1 +$:.unshift(File.dirname(__FILE__) + '/../../../rails/activesupport/lib')
  2 +$:.unshift(File.dirname(__FILE__) + '/../../../rails/activerecord/lib')
  3 +$:.unshift(File.dirname(__FILE__) + '/../lib')
  4 +require 'test/unit'
  5 +begin
  6 + require 'active_support'
  7 + require 'active_record'
  8 + require 'active_record/fixtures'
  9 +rescue LoadError
  10 + require 'rubygems'
  11 + retry
  12 +end
  13 +require 'acts_as_versioned'
  14 +
  15 +config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
  16 +ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
  17 +ActiveRecord::Base.configurations = {'test' => config[ENV['DB'] || 'sqlite']}
  18 +ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
  19 +
  20 +load(File.dirname(__FILE__) + "/schema.rb")
  21 +
  22 +# set up custom sequence on widget_versions for DBs that support sequences
  23 +if ENV['DB'] == 'postgresql'
  24 + ActiveRecord::Base.connection.execute "DROP SEQUENCE widgets_seq;" rescue nil
  25 + ActiveRecord::Base.connection.remove_column :widget_versions, :id
  26 + ActiveRecord::Base.connection.execute "CREATE SEQUENCE widgets_seq START 101;"
  27 + ActiveRecord::Base.connection.execute "ALTER TABLE widget_versions ADD COLUMN id INTEGER PRIMARY KEY DEFAULT nextval('widgets_seq');"
  28 +end
  29 +
  30 +Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
  31 +$:.unshift(Test::Unit::TestCase.fixture_path)
  32 +
  33 +class Test::Unit::TestCase #:nodoc:
  34 + # Turn off transactional fixtures if you're working with MyISAM tables in MySQL
  35 + self.use_transactional_fixtures = true
  36 +
  37 + # Instantiated fixtures are slow, but give you @david where you otherwise would need people(:david)
  38 + self.use_instantiated_fixtures = false
  39 +
  40 + # Add more helper methods to be used by all tests here...
  41 +end
0 \ No newline at end of file 42 \ No newline at end of file
vendor/plugins/acts_as_versioned/test/database.yml 0 → 100644
@@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
  1 +sqlite:
  2 + :adapter: sqlite
  3 + :dbfile: acts_as_versioned_plugin.sqlite.db
  4 +sqlite3:
  5 + :adapter: sqlite3
  6 + :dbfile: acts_as_versioned_plugin.sqlite3.db
  7 +postgresql:
  8 + :adapter: postgresql
  9 + :username: postgres
  10 + :password: postgres
  11 + :database: acts_as_versioned_plugin_test
  12 + :min_messages: ERROR
  13 +mysql:
  14 + :adapter: mysql
  15 + :host: localhost
  16 + :username: rails
  17 + :password:
  18 + :database: acts_as_versioned_plugin_test
0 \ No newline at end of file 19 \ No newline at end of file
vendor/plugins/acts_as_versioned/test/fixtures/authors.yml 0 → 100644
@@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
  1 +caged:
  2 + id: 1
  3 + name: caged
  4 +mly:
  5 + id: 2
  6 + name: mly
0 \ No newline at end of file 7 \ No newline at end of file
vendor/plugins/acts_as_versioned/test/fixtures/landmark.rb 0 → 100644
@@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
  1 +class Landmark < ActiveRecord::Base
  2 + acts_as_versioned :if_changed => [ :name, :longitude, :latitude ]
  3 +end
vendor/plugins/acts_as_versioned/test/fixtures/landmark_versions.yml 0 → 100644
@@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
  1 +washington:
  2 + id: 1
  3 + landmark_id: 1
  4 + version: 1
  5 + name: Washington, D.C.
  6 + latitude: 38.895
  7 + longitude: -77.036667
vendor/plugins/acts_as_versioned/test/fixtures/landmarks.yml 0 → 100644
@@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
  1 +washington:
  2 + id: 1
  3 + name: Washington, D.C.
  4 + latitude: 38.895
  5 + longitude: -77.036667
  6 + version: 1
vendor/plugins/acts_as_versioned/test/fixtures/locked_pages.yml 0 → 100644
@@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
  1 +welcome:
  2 + id: 1
  3 + title: Welcome to the weblog
  4 + lock_version: 24
  5 + type: LockedPage
  6 +thinking:
  7 + id: 2
  8 + title: So I was thinking
  9 + lock_version: 24
  10 + type: SpecialLockedPage
vendor/plugins/acts_as_versioned/test/fixtures/locked_pages_revisions.yml 0 → 100644
@@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
  1 +welcome_1:
  2 + id: 1
  3 + page_id: 1
  4 + title: Welcome to the weblg
  5 + version: 23
  6 + version_type: LockedPage
  7 +
  8 +welcome_2:
  9 + id: 2
  10 + page_id: 1
  11 + title: Welcome to the weblog
  12 + version: 24
  13 + version_type: LockedPage
  14 +
  15 +thinking_1:
  16 + id: 3
  17 + page_id: 2
  18 + title: So I was thinking!!!
  19 + version: 23
  20 + version_type: SpecialLockedPage
  21 +
  22 +thinking_2:
  23 + id: 4
  24 + page_id: 2
  25 + title: So I was thinking
  26 + version: 24
  27 + version_type: SpecialLockedPage
vendor/plugins/acts_as_versioned/test/fixtures/migrations/1_add_versioned_tables.rb 0 → 100644
@@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
  1 +class AddVersionedTables < ActiveRecord::Migration
  2 + def self.up
  3 + create_table("things") do |t|
  4 + t.column :title, :text
  5 + t.column :price, :decimal, :precision => 7, :scale => 2
  6 + t.column :type, :string
  7 + end
  8 + Thing.create_versioned_table
  9 + end
  10 +
  11 + def self.down
  12 + Thing.drop_versioned_table
  13 + drop_table "things" rescue nil
  14 + end
  15 +end
0 \ No newline at end of file 16 \ No newline at end of file
vendor/plugins/acts_as_versioned/test/fixtures/page.rb 0 → 100644
@@ -0,0 +1,43 @@ @@ -0,0 +1,43 @@
  1 +class Page < ActiveRecord::Base
  2 + belongs_to :author
  3 + has_many :authors, :through => :versions, :order => 'name'
  4 + belongs_to :revisor, :class_name => 'Author'
  5 + has_many :revisors, :class_name => 'Author', :through => :versions, :order => 'name'
  6 + acts_as_versioned :if => :feeling_good? do
  7 + def self.included(base)
  8 + base.cattr_accessor :feeling_good
  9 + base.feeling_good = true
  10 + base.belongs_to :author
  11 + base.belongs_to :revisor, :class_name => 'Author'
  12 + end
  13 +
  14 + def feeling_good?
  15 + @@feeling_good == true
  16 + end
  17 + end
  18 +end
  19 +
  20 +module LockedPageExtension
  21 + def hello_world
  22 + 'hello_world'
  23 + end
  24 +end
  25 +
  26 +class LockedPage < ActiveRecord::Base
  27 + acts_as_versioned \
  28 + :inheritance_column => :version_type,
  29 + :foreign_key => :page_id,
  30 + :table_name => :locked_pages_revisions,
  31 + :class_name => 'LockedPageRevision',
  32 + :version_column => :lock_version,
  33 + :limit => 2,
  34 + :if_changed => :title,
  35 + :extend => LockedPageExtension
  36 +end
  37 +
  38 +class SpecialLockedPage < LockedPage
  39 +end
  40 +
  41 +class Author < ActiveRecord::Base
  42 + has_many :pages
  43 +end
0 \ No newline at end of file 44 \ No newline at end of file
vendor/plugins/acts_as_versioned/test/fixtures/page_versions.yml 0 → 100644
@@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
  1 +welcome_2:
  2 + id: 1
  3 + page_id: 1
  4 + title: Welcome to the weblog
  5 + body: Such a lovely day
  6 + version: 24
  7 + author_id: 1
  8 + revisor_id: 1
  9 +welcome_1:
  10 + id: 2
  11 + page_id: 1
  12 + title: Welcome to the weblg
  13 + body: Such a lovely day
  14 + version: 23
  15 + author_id: 2
  16 + revisor_id: 2
vendor/plugins/acts_as_versioned/test/fixtures/pages.yml 0 → 100644
@@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
  1 +welcome:
  2 + id: 1
  3 + title: Welcome to the weblog
  4 + body: Such a lovely day
  5 + version: 24
  6 + author_id: 1
  7 + revisor_id: 1
0 \ No newline at end of file 8 \ No newline at end of file
vendor/plugins/acts_as_versioned/test/fixtures/widget.rb 0 → 100644
@@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
  1 +class Widget < ActiveRecord::Base
  2 + acts_as_versioned :sequence_name => 'widgets_seq', :association_options => {
  3 + :dependent => :nullify, :order => 'version desc'
  4 + }
  5 + non_versioned_columns << 'foo'
  6 +end
0 \ No newline at end of file 7 \ No newline at end of file
vendor/plugins/acts_as_versioned/test/migration_test.rb 0 → 100644
@@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
  1 +require File.join(File.dirname(__FILE__), 'abstract_unit')
  2 +
  3 +if ActiveRecord::Base.connection.supports_migrations?
  4 + class Thing < ActiveRecord::Base
  5 + attr_accessor :version
  6 + acts_as_versioned
  7 + end
  8 +
  9 + class MigrationTest < Test::Unit::TestCase
  10 + self.use_transactional_fixtures = false
  11 + def teardown
  12 + ActiveRecord::Base.connection.initialize_schema_information
  13 + ActiveRecord::Base.connection.update "UPDATE schema_info SET version = 0"
  14 +
  15 + Thing.connection.drop_table "things" rescue nil
  16 + Thing.connection.drop_table "thing_versions" rescue nil
  17 + Thing.reset_column_information
  18 + end
  19 +
  20 + def test_versioned_migration
  21 + assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' }
  22 + # take 'er up
  23 + ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/')
  24 + t = Thing.create :title => 'blah blah', :price => 123.45, :type => 'Thing'
  25 + assert_equal 1, t.versions.size
  26 +
  27 + # check that the price column has remembered its value correctly
  28 + assert_equal t.price, t.versions.first.price
  29 + assert_equal t.title, t.versions.first.title
  30 + assert_equal t[:type], t.versions.first[:type]
  31 +
  32 + # make sure that the precision of the price column has been preserved
  33 + assert_equal 7, Thing::Version.columns.find{|c| c.name == "price"}.precision
  34 + assert_equal 2, Thing::Version.columns.find{|c| c.name == "price"}.scale
  35 +
  36 + # now lets take 'er back down
  37 + ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/')
  38 + assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' }
  39 + end
  40 + end
  41 +end
vendor/plugins/acts_as_versioned/test/schema.rb 0 → 100644
@@ -0,0 +1,68 @@ @@ -0,0 +1,68 @@
  1 +ActiveRecord::Schema.define(:version => 0) do
  2 + create_table :pages, :force => true do |t|
  3 + t.column :version, :integer
  4 + t.column :title, :string, :limit => 255
  5 + t.column :body, :text
  6 + t.column :updated_on, :datetime
  7 + t.column :author_id, :integer
  8 + t.column :revisor_id, :integer
  9 + end
  10 +
  11 + create_table :page_versions, :force => true do |t|
  12 + t.column :page_id, :integer
  13 + t.column :version, :integer
  14 + t.column :title, :string, :limit => 255
  15 + t.column :body, :text
  16 + t.column :updated_on, :datetime
  17 + t.column :author_id, :integer
  18 + t.column :revisor_id, :integer
  19 + end
  20 +
  21 + create_table :authors, :force => true do |t|
  22 + t.column :page_id, :integer
  23 + t.column :name, :string
  24 + end
  25 +
  26 + create_table :locked_pages, :force => true do |t|
  27 + t.column :lock_version, :integer
  28 + t.column :title, :string, :limit => 255
  29 + t.column :type, :string, :limit => 255
  30 + end
  31 +
  32 + create_table :locked_pages_revisions, :force => true do |t|
  33 + t.column :page_id, :integer
  34 + t.column :version, :integer
  35 + t.column :title, :string, :limit => 255
  36 + t.column :version_type, :string, :limit => 255
  37 + t.column :updated_at, :datetime
  38 + end
  39 +
  40 + create_table :widgets, :force => true do |t|
  41 + t.column :name, :string, :limit => 50
  42 + t.column :foo, :string
  43 + t.column :version, :integer
  44 + t.column :updated_at, :datetime
  45 + end
  46 +
  47 + create_table :widget_versions, :force => true do |t|
  48 + t.column :widget_id, :integer
  49 + t.column :name, :string, :limit => 50
  50 + t.column :version, :integer
  51 + t.column :updated_at, :datetime
  52 + end
  53 +
  54 + create_table :landmarks, :force => true do |t|
  55 + t.column :name, :string
  56 + t.column :latitude, :float
  57 + t.column :longitude, :float
  58 + t.column :version, :integer
  59 + end
  60 +
  61 + create_table :landmark_versions, :force => true do |t|
  62 + t.column :landmark_id, :integer
  63 + t.column :name, :string
  64 + t.column :latitude, :float
  65 + t.column :longitude, :float
  66 + t.column :version, :integer
  67 + end
  68 +end
vendor/plugins/acts_as_versioned/test/versioned_test.rb 0 → 100644
@@ -0,0 +1,328 @@ @@ -0,0 +1,328 @@
  1 +require File.join(File.dirname(__FILE__), 'abstract_unit')
  2 +require File.join(File.dirname(__FILE__), 'fixtures/page')
  3 +require File.join(File.dirname(__FILE__), 'fixtures/widget')
  4 +
  5 +class VersionedTest < Test::Unit::TestCase
  6 + fixtures :pages, :page_versions, :locked_pages, :locked_pages_revisions, :authors, :landmarks, :landmark_versions
  7 + set_fixture_class :page_versions => Page::Version
  8 +
  9 + def test_saves_versioned_copy
  10 + p = Page.create! :title => 'first title', :body => 'first body'
  11 + assert !p.new_record?
  12 + assert_equal 1, p.versions.size
  13 + assert_equal 1, p.version
  14 + assert_instance_of Page.versioned_class, p.versions.first
  15 + end
  16 +
  17 + def test_saves_without_revision
  18 + p = pages(:welcome)
  19 + old_versions = p.versions.count
  20 +
  21 + p.save_without_revision
  22 +
  23 + p.without_revision do
  24 + p.update_attributes :title => 'changed'
  25 + end
  26 +
  27 + assert_equal old_versions, p.versions.count
  28 + end
  29 +
  30 + def test_rollback_with_version_number
  31 + p = pages(:welcome)
  32 + assert_equal 24, p.version
  33 + assert_equal 'Welcome to the weblog', p.title
  34 +
  35 + assert p.revert_to!(p.versions.first.version), "Couldn't revert to 23"
  36 + assert_equal 23, p.version
  37 + assert_equal 'Welcome to the weblg', p.title
  38 + end
  39 +
  40 + def test_versioned_class_name
  41 + assert_equal 'Version', Page.versioned_class_name
  42 + assert_equal 'LockedPageRevision', LockedPage.versioned_class_name
  43 + end
  44 +
  45 + def test_versioned_class
  46 + assert_equal Page::Version, Page.versioned_class
  47 + assert_equal LockedPage::LockedPageRevision, LockedPage.versioned_class
  48 + end
  49 +
  50 + def test_special_methods
  51 + assert_nothing_raised { pages(:welcome).feeling_good? }
  52 + assert_nothing_raised { pages(:welcome).versions.first.feeling_good? }
  53 + assert_nothing_raised { locked_pages(:welcome).hello_world }
  54 + assert_nothing_raised { locked_pages(:welcome).versions.first.hello_world }
  55 + end
  56 +
  57 + def test_rollback_with_version_class
  58 + p = pages(:welcome)
  59 + assert_equal 24, p.version
  60 + assert_equal 'Welcome to the weblog', p.title
  61 +
  62 + assert p.revert_to!(p.versions.first), "Couldn't revert to 23"
  63 + assert_equal 23, p.version
  64 + assert_equal 'Welcome to the weblg', p.title
  65 + end
  66 +
  67 + def test_rollback_fails_with_invalid_revision
  68 + p = locked_pages(:welcome)
  69 + assert !p.revert_to!(locked_pages(:thinking))
  70 + end
  71 +
  72 + def test_saves_versioned_copy_with_options
  73 + p = LockedPage.create! :title => 'first title'
  74 + assert !p.new_record?
  75 + assert_equal 1, p.versions.size
  76 + assert_instance_of LockedPage.versioned_class, p.versions.first
  77 + end
  78 +
  79 + def test_rollback_with_version_number_with_options
  80 + p = locked_pages(:welcome)
  81 + assert_equal 'Welcome to the weblog', p.title
  82 + assert_equal 'LockedPage', p.versions.first.version_type
  83 +
  84 + assert p.revert_to!(p.versions.first.version), "Couldn't revert to 23"
  85 + assert_equal 'Welcome to the weblg', p.title
  86 + assert_equal 'LockedPage', p.versions.first.version_type
  87 + end
  88 +
  89 + def test_rollback_with_version_class_with_options
  90 + p = locked_pages(:welcome)
  91 + assert_equal 'Welcome to the weblog', p.title
  92 + assert_equal 'LockedPage', p.versions.first.version_type
  93 +
  94 + assert p.revert_to!(p.versions.first), "Couldn't revert to 1"
  95 + assert_equal 'Welcome to the weblg', p.title
  96 + assert_equal 'LockedPage', p.versions.first.version_type
  97 + end
  98 +
  99 + def test_saves_versioned_copy_with_sti
  100 + p = SpecialLockedPage.create! :title => 'first title'
  101 + assert !p.new_record?
  102 + assert_equal 1, p.versions.size
  103 + assert_instance_of LockedPage.versioned_class, p.versions.first
  104 + assert_equal 'SpecialLockedPage', p.versions.first.version_type
  105 + end
  106 +
  107 + def test_rollback_with_version_number_with_sti
  108 + p = locked_pages(:thinking)
  109 + assert_equal 'So I was thinking', p.title
  110 +
  111 + assert p.revert_to!(p.versions.first.version), "Couldn't revert to 1"
  112 + assert_equal 'So I was thinking!!!', p.title
  113 + assert_equal 'SpecialLockedPage', p.versions.first.version_type
  114 + end
  115 +
  116 + def test_lock_version_works_with_versioning
  117 + p = locked_pages(:thinking)
  118 + p2 = LockedPage.find(p.id)
  119 +
  120 + p.title = 'fresh title'
  121 + p.save
  122 + assert_equal 2, p.versions.size # limit!
  123 +
  124 + assert_raises(ActiveRecord::StaleObjectError) do
  125 + p2.title = 'stale title'
  126 + p2.save
  127 + end
  128 + end
  129 +
  130 + def test_version_if_condition
  131 + p = Page.create! :title => "title"
  132 + assert_equal 1, p.version
  133 +
  134 + Page.feeling_good = false
  135 + p.save
  136 + assert_equal 1, p.version
  137 + Page.feeling_good = true
  138 + end
  139 +
  140 + def test_version_if_condition2
  141 + # set new if condition
  142 + Page.class_eval do
  143 + def new_feeling_good() title[0..0] == 'a'; end
  144 + alias_method :old_feeling_good, :feeling_good?
  145 + alias_method :feeling_good?, :new_feeling_good
  146 + end
  147 +
  148 + p = Page.create! :title => "title"
  149 + assert_equal 1, p.version # version does not increment
  150 + assert_equal 1, p.versions(true).size
  151 +
  152 + p.update_attributes(:title => 'new title')
  153 + assert_equal 1, p.version # version does not increment
  154 + assert_equal 1, p.versions(true).size
  155 +
  156 + p.update_attributes(:title => 'a title')
  157 + assert_equal 2, p.version
  158 + assert_equal 2, p.versions(true).size
  159 +
  160 + # reset original if condition
  161 + Page.class_eval { alias_method :feeling_good?, :old_feeling_good }
  162 + end
  163 +
  164 + def test_version_if_condition_with_block
  165 + # set new if condition
  166 + old_condition = Page.version_condition
  167 + Page.version_condition = Proc.new { |page| page.title[0..0] == 'b' }
  168 +
  169 + p = Page.create! :title => "title"
  170 + assert_equal 1, p.version # version does not increment
  171 + assert_equal 1, p.versions(true).size
  172 +
  173 + p.update_attributes(:title => 'a title')
  174 + assert_equal 1, p.version # version does not increment
  175 + assert_equal 1, p.versions(true).size
  176 +
  177 + p.update_attributes(:title => 'b title')
  178 + assert_equal 2, p.version
  179 + assert_equal 2, p.versions(true).size
  180 +
  181 + # reset original if condition
  182 + Page.version_condition = old_condition
  183 + end
  184 +
  185 + def test_version_no_limit
  186 + p = Page.create! :title => "title", :body => 'first body'
  187 + p.save
  188 + p.save
  189 + 5.times do |i|
  190 + assert_page_title p, i
  191 + end
  192 + end
  193 +
  194 + def test_version_max_limit
  195 + p = LockedPage.create! :title => "title"
  196 + p.update_attributes(:title => "title1")
  197 + p.update_attributes(:title => "title2")
  198 + 5.times do |i|
  199 + assert_page_title p, i, :lock_version
  200 + assert p.versions(true).size <= 2, "locked version can only store 2 versions"
  201 + end
  202 + end
  203 +
  204 + def test_track_changed_attributes_default_value
  205 + assert !Page.track_changed_attributes
  206 + assert LockedPage.track_changed_attributes
  207 + assert SpecialLockedPage.track_changed_attributes
  208 + end
  209 +
  210 + def test_version_order
  211 + assert_equal 23, pages(:welcome).versions.first.version
  212 + assert_equal 24, pages(:welcome).versions.last.version
  213 + end
  214 +
  215 + def test_track_changed_attributes
  216 + p = LockedPage.create! :title => "title"
  217 + assert_equal 1, p.lock_version
  218 + assert_equal 1, p.versions(true).size
  219 +
  220 + p.title = 'title'
  221 + assert !p.save_version?
  222 + p.save
  223 + assert_equal 2, p.lock_version # still increments version because of optimistic locking
  224 + assert_equal 1, p.versions(true).size
  225 +
  226 + p.title = 'updated title'
  227 + assert p.save_version?
  228 + p.save
  229 + assert_equal 3, p.lock_version
  230 + assert_equal 1, p.versions(true).size # version 1 deleted
  231 +
  232 + p.title = 'updated title!'
  233 + assert p.save_version?
  234 + p.save
  235 + assert_equal 4, p.lock_version
  236 + assert_equal 2, p.versions(true).size # version 1 deleted
  237 + end
  238 +
  239 + def assert_page_title(p, i, version_field = :version)
  240 + p.title = "title#{i}"
  241 + p.save
  242 + assert_equal "title#{i}", p.title
  243 + assert_equal (i+4), p.send(version_field)
  244 + end
  245 +
  246 + def test_find_versions
  247 + assert_equal 2, locked_pages(:welcome).versions.size
  248 + assert_equal 1, locked_pages(:welcome).versions.find(:all, :conditions => ['title LIKE ?', '%weblog%']).length
  249 + assert_equal 2, locked_pages(:welcome).versions.find(:all, :conditions => ['title LIKE ?', '%web%']).length
  250 + assert_equal 0, locked_pages(:thinking).versions.find(:all, :conditions => ['title LIKE ?', '%web%']).length
  251 + assert_equal 2, locked_pages(:welcome).versions.length
  252 + end
  253 +
  254 + def test_with_sequence
  255 + assert_equal 'widgets_seq', Widget.versioned_class.sequence_name
  256 + 3.times { Widget.create! :name => 'new widget' }
  257 + assert_equal 3, Widget.count
  258 + assert_equal 3, Widget.versioned_class.count
  259 + end
  260 +
  261 + def test_has_many_through
  262 + assert_equal [authors(:caged), authors(:mly)], pages(:welcome).authors
  263 + end
  264 +
  265 + def test_has_many_through_with_custom_association
  266 + assert_equal [authors(:caged), authors(:mly)], pages(:welcome).revisors
  267 + end
  268 +
  269 + def test_referential_integrity
  270 + pages(:welcome).destroy
  271 + assert_equal 0, Page.count
  272 + assert_equal 0, Page::Version.count
  273 + end
  274 +
  275 + def test_association_options
  276 + association = Page.reflect_on_association(:versions)
  277 + options = association.options
  278 + assert_equal :delete_all, options[:dependent]
  279 + assert_equal 'version', options[:order]
  280 +
  281 + association = Widget.reflect_on_association(:versions)
  282 + options = association.options
  283 + assert_equal :nullify, options[:dependent]
  284 + assert_equal 'version desc', options[:order]
  285 + assert_equal 'widget_id', options[:foreign_key]
  286 +
  287 + widget = Widget.create! :name => 'new widget'
  288 + assert_equal 1, Widget.count
  289 + assert_equal 1, Widget.versioned_class.count
  290 + widget.destroy
  291 + assert_equal 0, Widget.count
  292 + assert_equal 1, Widget.versioned_class.count
  293 + end
  294 +
  295 + def test_versioned_records_should_belong_to_parent
  296 + page = pages(:welcome)
  297 + page_version = page.versions.last
  298 + assert_equal page, page_version.page
  299 + end
  300 +
  301 + def test_unchanged_attributes
  302 + landmarks(:washington).attributes = landmarks(:washington).attributes.except("id")
  303 + assert !landmarks(:washington).changed?
  304 + end
  305 +
  306 + def test_unchanged_string_attributes
  307 + landmarks(:washington).attributes = landmarks(:washington).attributes.except("id").inject({}) { |params, (key, value)| params.update(key => value.to_s) }
  308 + assert !landmarks(:washington).changed?
  309 + end
  310 +
  311 + def test_should_find_earliest_version
  312 + assert_equal page_versions(:welcome_1), pages(:welcome).versions.earliest
  313 + end
  314 +
  315 + def test_should_find_latest_version
  316 + assert_equal page_versions(:welcome_2), pages(:welcome).versions.latest
  317 + end
  318 +
  319 + def test_should_find_previous_version
  320 + assert_equal page_versions(:welcome_1), page_versions(:welcome_2).previous
  321 + assert_equal page_versions(:welcome_1), pages(:welcome).versions.before(page_versions(:welcome_2))
  322 + end
  323 +
  324 + def test_should_find_next_version
  325 + assert_equal page_versions(:welcome_2), page_versions(:welcome_1).next
  326 + assert_equal page_versions(:welcome_2), pages(:welcome).versions.after(page_versions(:welcome_1))
  327 + end
  328 +end
0 \ No newline at end of file 329 \ No newline at end of file
vendor/plugins/attachment_fu/CHANGELOG 0 → 100644
@@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
  1 +* April 2, 2007 *
  2 +
  3 +* don't copy the #full_filename to the default #temp_paths array if it doesn't exist
  4 +* add default ID partitioning for attachments
  5 +* add #binmode call to Tempfile (note: ruby should be doing this!) [Eric Beland]
  6 +* Check for current type of :thumbnails option.
  7 +* allow customization of the S3 configuration file path with the :s3_config_path option.
  8 +* Don't try to remove thumbnails if there aren't any. Closes #3 [ben stiglitz]
  9 +
  10 +* BC * (before changelog)
  11 +
  12 +* add default #temp_paths entry [mattly]
  13 +* add MiniMagick support to attachment_fu [Isacc]
  14 +* update #destroy_file to clear out any empty directories too [carlivar]
  15 +* fix references to S3Backend module [Hunter Hillegas]
  16 +* make #current_data public with db_file and s3 backends [ebryn]
  17 +* oops, actually svn add the files for s3 backend. [Jeffrey Hardy]
  18 +* experimental s3 support, egad, no tests.... [Jeffrey Hardy]
  19 +* doh, fix a few bad references to ActsAsAttachment [sixty4bit]
0 \ No newline at end of file 20 \ No newline at end of file
vendor/plugins/attachment_fu/README 0 → 100644
@@ -0,0 +1,162 @@ @@ -0,0 +1,162 @@
  1 +attachment-fu
  2 +=====================
  3 +
  4 +attachment_fu is a plugin by Rick Olson (aka technoweenie <http://techno-weenie.net>) and is the successor to acts_as_attachment. To get a basic run-through of its capabilities, check out Mike Clark's tutorial <http://clarkware.com/cgi/blosxom/2007/02/24#FileUploadFu>.
  5 +
  6 +
  7 +attachment_fu functionality
  8 +===========================
  9 +
  10 +attachment_fu facilitates file uploads in Ruby on Rails. There are a few storage options for the actual file data, but the plugin always at a minimum stores metadata for each file in the database.
  11 +
  12 +There are three storage options for files uploaded through attachment_fu:
  13 + File system
  14 + Database file
  15 + Amazon S3
  16 +
  17 +Each method of storage many options associated with it that will be covered in the following section. Something to note, however, is that the Amazon S3 storage requires you to modify config/amazon_s3.yml and the Database file storage requires an extra table.
  18 +
  19 +
  20 +attachment_fu models
  21 +====================
  22 +
  23 +For all three of these storage options a table of metadata is required. This table will contain information about the file (hence the 'meta') and its location. This table has no restrictions on naming, unlike the extra table required for database storage, which must have a table name of db_files (and by convention a model of DbFile).
  24 +
  25 +In the model there are two methods made available by this plugins: has_attachment and validates_as_attachment.
  26 +
  27 +has_attachment(options = {})
  28 + This method accepts the options in a hash:
  29 + :content_type # Allowed content types.
  30 + # Allows all by default. Use :image to allow all standard image types.
  31 + :min_size # Minimum size allowed.
  32 + # 1 byte is the default.
  33 + :max_size # Maximum size allowed.
  34 + # 1.megabyte is the default.
  35 + :size # Range of sizes allowed.
  36 + # (1..1.megabyte) is the default. This overrides the :min_size and :max_size options.
  37 + :resize_to # Used by RMagick to resize images.
  38 + # Pass either an array of width/height, or a geometry string.
  39 + :thumbnails # Specifies a set of thumbnails to generate.
  40 + # This accepts a hash of filename suffixes and RMagick resizing options.
  41 + # This option need only be included if you want thumbnailing.
  42 + :thumbnail_class # Set which model class to use for thumbnails.
  43 + # This current attachment class is used by default.
  44 + :path_prefix # path to store the uploaded files.
  45 + # Uses public/#{table_name} by default for the filesystem, and just #{table_name} for the S3 backend.
  46 + # Setting this sets the :storage to :file_system.
  47 + :storage # Specifies the storage system to use..
  48 + # Defaults to :db_file. Options are :file_system, :db_file, and :s3.
  49 + :processor # Sets the image processor to use for resizing of the attached image.
  50 + # Options include ImageScience, Rmagick, and MiniMagick. Default is whatever is installed.
  51 +
  52 +
  53 + Examples:
  54 + has_attachment :max_size => 1.kilobyte
  55 + has_attachment :size => 1.megabyte..2.megabytes
  56 + has_attachment :content_type => 'application/pdf'
  57 + has_attachment :content_type => ['application/pdf', 'application/msword', 'text/plain']
  58 + has_attachment :content_type => :image, :resize_to => [50,50]
  59 + has_attachment :content_type => ['application/pdf', :image], :resize_to => 'x50'
  60 + has_attachment :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
  61 + has_attachment :storage => :file_system, :path_prefix => 'public/files'
  62 + has_attachment :storage => :file_system, :path_prefix => 'public/files',
  63 + :content_type => :image, :resize_to => [50,50]
  64 + has_attachment :storage => :file_system, :path_prefix => 'public/files',
  65 + :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
  66 + has_attachment :storage => :s3
  67 +
  68 +validates_as_attachment
  69 + This method prevents files outside of the valid range (:min_size to :max_size, or the :size range) from being saved. It does not however, halt the upload of such files. They will be uploaded into memory regardless of size before validation.
  70 +
  71 + Example:
  72 + validates_as_attachment
  73 +
  74 +
  75 +attachment_fu migrations
  76 +========================
  77 +
  78 +Fields for attachment_fu metadata tables...
  79 + in general:
  80 + size, :integer # file size in bytes
  81 + content_type, :string # mime type, ex: application/mp3
  82 + filename, :string # sanitized filename
  83 + that reference images:
  84 + height, :integer # in pixels
  85 + width, :integer # in pixels
  86 + that reference images that will be thumbnailed:
  87 + parent_id, :integer # id of parent image (on the same table, a self-referencing foreign-key).
  88 + # Only populated if the current object is a thumbnail.
  89 + thumbnail, :string # the 'type' of thumbnail this attachment record describes.
  90 + # Only populated if the current object is a thumbnail.
  91 + # Usage:
  92 + # [ In Model 'Avatar' ]
  93 + # has_attachment :content_type => :image,
  94 + # :storage => :file_system,
  95 + # :max_size => 500.kilobytes,
  96 + # :resize_to => '320x200>',
  97 + # :thumbnails => { :small => '10x10>',
  98 + # :thumb => '100x100>' }
  99 + # [ Elsewhere ]
  100 + # @user.avatar.thumbnails.first.thumbnail #=> 'small'
  101 + that reference files stored in the database (:db_file):
  102 + db_file_id, :integer # id of the file in the database (foreign key)
  103 +
  104 +Field for attachment_fu db_files table:
  105 + data, :binary # binary file data, for use in database file storage
  106 +
  107 +
  108 +attachment_fu views
  109 +===================
  110 +
  111 +There are two main views tasks that will be directly affected by attachment_fu: upload forms and displaying uploaded images.
  112 +
  113 +There are two parts of the upload form that differ from typical usage.
  114 + 1. Include ':multipart => true' in the html options of the form_for tag.
  115 + Example:
  116 + <% form_for(:attachment_metadata, :url => { :action => "create" }, :html => { :multipart => true }) do |form| %>
  117 +
  118 + 2. Use the file_field helper with :uploaded_data as the field name.
  119 + Example:
  120 + <%= form.file_field :uploaded_data %>
  121 +
  122 +Displaying uploaded images is made easy by the public_filename method of the ActiveRecord attachment objects using file system and s3 storage.
  123 +
  124 +public_filename(thumbnail = nil)
  125 + Returns the public path to the file. If a thumbnail prefix is specified it will return the public file path to the corresponding thumbnail.
  126 + Examples:
  127 + attachment_obj.public_filename #=> /attachments/2/file.jpg
  128 + attachment_obj.public_filename(:thumb) #=> /attachments/2/file_thumb.jpg
  129 + attachment_obj.public_filename(:small) #=> /attachments/2/file_small.jpg
  130 +
  131 +When serving files from database storage, doing more than simply downloading the file is beyond the scope of this document.
  132 +
  133 +
  134 +attachment_fu controllers
  135 +=========================
  136 +
  137 +There are two considerations to take into account when using attachment_fu in controllers.
  138 +
  139 +The first is when the files have no publicly accessible path and need to be downloaded through an action.
  140 +
  141 +Example:
  142 + def readme
  143 + send_file '/path/to/readme.txt', :type => 'plain/text', :disposition => 'inline'
  144 + end
  145 +
  146 +See the possible values for send_file for reference.
  147 +
  148 +
  149 +The second is when saving the file when submitted from a form.
  150 +Example in view:
  151 + <%= form.file_field :attachable, :uploaded_data %>
  152 +
  153 +Example in controller:
  154 + def create
  155 + @attachable_file = AttachmentMetadataModel.new(params[:attachable])
  156 + if @attachable_file.save
  157 + flash[:notice] = 'Attachment was successfully created.'
  158 + redirect_to attachable_url(@attachable_file)
  159 + else
  160 + render :action => :new
  161 + end
  162 + end
vendor/plugins/attachment_fu/Rakefile 0 → 100644
@@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
  1 +require 'rake'
  2 +require 'rake/testtask'
  3 +require 'rake/rdoctask'
  4 +
  5 +desc 'Default: run unit tests.'
  6 +task :default => :test
  7 +
  8 +desc 'Test the attachment_fu plugin.'
  9 +Rake::TestTask.new(:test) do |t|
  10 + t.libs << 'lib'
  11 + t.pattern = 'test/**/*_test.rb'
  12 + t.verbose = true
  13 +end
  14 +
  15 +desc 'Generate documentation for the attachment_fu plugin.'
  16 +Rake::RDocTask.new(:rdoc) do |rdoc|
  17 + rdoc.rdoc_dir = 'rdoc'
  18 + rdoc.title = 'ActsAsAttachment'
  19 + rdoc.options << '--line-numbers --inline-source'
  20 + rdoc.rdoc_files.include('README')
  21 + rdoc.rdoc_files.include('lib/**/*.rb')
  22 +end
vendor/plugins/attachment_fu/amazon_s3.yml.tpl 0 → 100644
@@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
  1 +development:
  2 + bucket_name: appname_development
  3 + access_key_id:
  4 + secret_access_key:
  5 +
  6 +test:
  7 + bucket_name: appname_test
  8 + access_key_id:
  9 + secret_access_key:
  10 +
  11 +production:
  12 + bucket_name: appname
  13 + access_key_id:
  14 + secret_access_key:
vendor/plugins/attachment_fu/init.rb 0 → 100644
@@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
  1 +require 'tempfile'
  2 +
  3 +Tempfile.class_eval do
  4 + # overwrite so tempfiles use the extension of the basename. important for rmagick and image science
  5 + def make_tmpname(basename, n)
  6 + ext = nil
  7 + sprintf("%s%d-%d%s", basename.to_s.gsub(/\.\w+$/) { |s| ext = s; '' }, $$, n, ext)
  8 + end
  9 +end
  10 +
  11 +require 'geometry'
  12 +ActiveRecord::Base.send(:extend, Technoweenie::AttachmentFu::ActMethods)
  13 +Technoweenie::AttachmentFu.tempfile_path = ATTACHMENT_FU_TEMPFILE_PATH if Object.const_defined?(:ATTACHMENT_FU_TEMPFILE_PATH)
  14 +FileUtils.mkdir_p Technoweenie::AttachmentFu.tempfile_path
0 \ No newline at end of file 15 \ No newline at end of file
vendor/plugins/attachment_fu/install.rb 0 → 100644
@@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
  1 +require 'fileutils'
  2 +
  3 +s3_config = File.dirname(__FILE__) + '/../../../config/amazon_s3.yml'
  4 +FileUtils.cp File.dirname(__FILE__) + '/amazon_s3.yml.tpl', s3_config unless File.exist?(s3_config)
  5 +puts IO.read(File.join(File.dirname(__FILE__), 'README'))
0 \ No newline at end of file 6 \ No newline at end of file
vendor/plugins/attachment_fu/lib/geometry.rb 0 → 100644
@@ -0,0 +1,93 @@ @@ -0,0 +1,93 @@
  1 +# This Geometry class was yanked from RMagick. However, it lets ImageMagick handle the actual change_geometry.
  2 +# Use #new_dimensions_for to get new dimensons
  3 +# Used so I can use spiffy RMagick geometry strings with ImageScience
  4 +class Geometry
  5 + # ! and @ are removed until support for them is added
  6 + FLAGS = ['', '%', '<', '>']#, '!', '@']
  7 + RFLAGS = { '%' => :percent,
  8 + '!' => :aspect,
  9 + '<' => :>,
  10 + '>' => :<,
  11 + '@' => :area }
  12 +
  13 + attr_accessor :width, :height, :x, :y, :flag
  14 +
  15 + def initialize(width=nil, height=nil, x=nil, y=nil, flag=nil)
  16 + # Support floating-point width and height arguments so Geometry
  17 + # objects can be used to specify Image#density= arguments.
  18 + raise ArgumentError, "width must be >= 0: #{width}" if width < 0
  19 + raise ArgumentError, "height must be >= 0: #{height}" if height < 0
  20 + @width = width.to_f
  21 + @height = height.to_f
  22 + @x = x.to_i
  23 + @y = y.to_i
  24 + @flag = flag
  25 + end
  26 +
  27 + # Construct an object from a geometry string
  28 + RE = /\A(\d*)(?:x(\d+))?([-+]\d+)?([-+]\d+)?([%!<>@]?)\Z/
  29 +
  30 + def self.from_s(str)
  31 + raise(ArgumentError, "no geometry string specified") unless str
  32 +
  33 + if m = RE.match(str)
  34 + new(m[1].to_i, m[2].to_i, m[3].to_i, m[4].to_i, RFLAGS[m[5]])
  35 + else
  36 + raise ArgumentError, "invalid geometry format"
  37 + end
  38 + end
  39 +
  40 + # Convert object to a geometry string
  41 + def to_s
  42 + str = ''
  43 + str << "%g" % @width if @width > 0
  44 + str << 'x' if (@width > 0 || @height > 0)
  45 + str << "%g" % @height if @height > 0
  46 + str << "%+d%+d" % [@x, @y] if (@x != 0 || @y != 0)
  47 + str << FLAGS[@flag.to_i]
  48 + end
  49 +
  50 + # attempts to get new dimensions for the current geometry string given these old dimensions.
  51 + # This doesn't implement the aspect flag (!) or the area flag (@). PDI
  52 + def new_dimensions_for(orig_width, orig_height)
  53 + new_width = orig_width
  54 + new_height = orig_height
  55 +
  56 + case @flag
  57 + when :percent
  58 + scale_x = @width.zero? ? 100 : @width
  59 + scale_y = @height.zero? ? @width : @height
  60 + new_width = scale_x.to_f * (orig_width.to_f / 100.0)
  61 + new_height = scale_y.to_f * (orig_height.to_f / 100.0)
  62 + when :<, :>, nil
  63 + scale_factor =
  64 + if new_width.zero? || new_height.zero?
  65 + 1.0
  66 + else
  67 + if @width.nonzero? && @height.nonzero?
  68 + [@width.to_f / new_width.to_f, @height.to_f / new_height.to_f].min
  69 + else
  70 + @width.nonzero? ? (@width.to_f / new_width.to_f) : (@height.to_f / new_height.to_f)
  71 + end
  72 + end
  73 + new_width = scale_factor * new_width.to_f
  74 + new_height = scale_factor * new_height.to_f
  75 + new_width = orig_width if @flag && orig_width.send(@flag, new_width)
  76 + new_height = orig_height if @flag && orig_height.send(@flag, new_height)
  77 + end
  78 +
  79 + [new_width, new_height].collect! { |v| v.round }
  80 + end
  81 +end
  82 +
  83 +class Array
  84 + # allows you to get new dimensions for the current array of dimensions with a given geometry string
  85 + #
  86 + # [50, 64] / '40>' # => [40, 51]
  87 + def /(geometry)
  88 + raise ArgumentError, "Only works with a [width, height] pair" if size != 2
  89 + raise ArgumentError, "Must pass a valid geometry string or object" unless geometry.is_a?(String) || geometry.is_a?(Geometry)
  90 + geometry = Geometry.from_s(geometry) if geometry.is_a?(String)
  91 + geometry.new_dimensions_for first, last
  92 + end
  93 +end
0 \ No newline at end of file 94 \ No newline at end of file
vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu.rb 0 → 100644
@@ -0,0 +1,405 @@ @@ -0,0 +1,405 @@
  1 +module Technoweenie # :nodoc:
  2 + module AttachmentFu # :nodoc:
  3 + @@default_processors = %w(ImageScience Rmagick MiniMagick)
  4 + @@tempfile_path = File.join(RAILS_ROOT, 'tmp', 'attachment_fu')
  5 + @@content_types = ['image/jpeg', 'image/pjpeg', 'image/gif', 'image/png', 'image/x-png', 'image/jpg']
  6 + mattr_reader :content_types, :tempfile_path, :default_processors
  7 + mattr_writer :tempfile_path
  8 +
  9 + class ThumbnailError < StandardError; end
  10 + class AttachmentError < StandardError; end
  11 +
  12 + module ActMethods
  13 + # Options:
  14 + # * <tt>:content_type</tt> - Allowed content types. Allows all by default. Use :image to allow all standard image types.
  15 + # * <tt>:min_size</tt> - Minimum size allowed. 1 byte is the default.
  16 + # * <tt>:max_size</tt> - Maximum size allowed. 1.megabyte is the default.
  17 + # * <tt>:size</tt> - Range of sizes allowed. (1..1.megabyte) is the default. This overrides the :min_size and :max_size options.
  18 + # * <tt>:resize_to</tt> - Used by RMagick to resize images. Pass either an array of width/height, or a geometry string.
  19 + # * <tt>:thumbnails</tt> - Specifies a set of thumbnails to generate. This accepts a hash of filename suffixes and RMagick resizing options.
  20 + # * <tt>:thumbnail_class</tt> - Set what class to use for thumbnails. This attachment class is used by default.
  21 + # * <tt>:path_prefix</tt> - path to store the uploaded files. Uses public/#{table_name} by default for the filesystem, and just #{table_name}
  22 + # for the S3 backend. Setting this sets the :storage to :file_system.
  23 + # * <tt>:storage</tt> - Use :file_system to specify the attachment data is stored with the file system. Defaults to :db_system.
  24 + #
  25 + # Examples:
  26 + # has_attachment :max_size => 1.kilobyte
  27 + # has_attachment :size => 1.megabyte..2.megabytes
  28 + # has_attachment :content_type => 'application/pdf'
  29 + # has_attachment :content_type => ['application/pdf', 'application/msword', 'text/plain']
  30 + # has_attachment :content_type => :image, :resize_to => [50,50]
  31 + # has_attachment :content_type => ['application/pdf', :image], :resize_to => 'x50'
  32 + # has_attachment :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
  33 + # has_attachment :storage => :file_system, :path_prefix => 'public/files'
  34 + # has_attachment :storage => :file_system, :path_prefix => 'public/files',
  35 + # :content_type => :image, :resize_to => [50,50]
  36 + # has_attachment :storage => :file_system, :path_prefix => 'public/files',
  37 + # :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
  38 + # has_attachment :storage => :s3
  39 + def has_attachment(options = {})
  40 + # this allows you to redefine the acts' options for each subclass, however
  41 + options[:min_size] ||= 1
  42 + options[:max_size] ||= 1.megabyte
  43 + options[:size] ||= (options[:min_size]..options[:max_size])
  44 + options[:thumbnails] ||= {}
  45 + options[:thumbnail_class] ||= self
  46 + options[:s3_access] ||= :public_read
  47 + options[:content_type] = [options[:content_type]].flatten.collect! { |t| t == :image ? Technoweenie::AttachmentFu.content_types : t }.flatten unless options[:content_type].nil?
  48 +
  49 + unless options[:thumbnails].is_a?(Hash)
  50 + raise ArgumentError, ":thumbnails option should be a hash: e.g. :thumbnails => { :foo => '50x50' }"
  51 + end
  52 +
  53 + # doing these shenanigans so that #attachment_options is available to processors and backends
  54 + class_inheritable_accessor :attachment_options
  55 + self.attachment_options = options
  56 +
  57 + # only need to define these once on a class
  58 + unless included_modules.include?(InstanceMethods)
  59 + attr_accessor :thumbnail_resize_options
  60 +
  61 + attachment_options[:storage] ||= (attachment_options[:file_system_path] || attachment_options[:path_prefix]) ? :file_system : :db_file
  62 + attachment_options[:path_prefix] ||= attachment_options[:file_system_path]
  63 + if attachment_options[:path_prefix].nil?
  64 + attachment_options[:path_prefix] = attachment_options[:storage] == :s3 ? table_name : File.join("public", table_name)
  65 + end
  66 + attachment_options[:path_prefix] = attachment_options[:path_prefix][1..-1] if options[:path_prefix].first == '/'
  67 +
  68 + with_options :foreign_key => 'parent_id' do |m|
  69 + m.has_many :thumbnails, :class_name => attachment_options[:thumbnail_class].to_s
  70 + m.belongs_to :parent, :class_name => base_class.to_s
  71 + end
  72 + before_destroy :destroy_thumbnails
  73 +
  74 + before_validation :set_size_from_temp_path
  75 + after_save :after_process_attachment
  76 + after_destroy :destroy_file
  77 + extend ClassMethods
  78 + include InstanceMethods
  79 + include Technoweenie::AttachmentFu::Backends.const_get("#{options[:storage].to_s.classify}Backend")
  80 + case attachment_options[:processor]
  81 + when :none
  82 + when nil
  83 + processors = Technoweenie::AttachmentFu.default_processors.dup
  84 + begin
  85 + include Technoweenie::AttachmentFu::Processors.const_get("#{processors.first}Processor") if processors.any?
  86 + rescue LoadError, MissingSourceFile
  87 + processors.shift
  88 + retry
  89 + end
  90 + else
  91 + begin
  92 + include Technoweenie::AttachmentFu::Processors.const_get("#{options[:processor].to_s.classify}Processor")
  93 + rescue LoadError, MissingSourceFile
  94 + puts "Problems loading #{options[:processor]}Processor: #{$!}"
  95 + end
  96 + end
  97 + after_validation :process_attachment
  98 + end
  99 + end
  100 + end
  101 +
  102 + module ClassMethods
  103 + delegate :content_types, :to => Technoweenie::AttachmentFu
  104 +
  105 + # Performs common validations for attachment models.
  106 + def validates_as_attachment
  107 + validates_presence_of :size, :content_type, :filename
  108 + validate :attachment_attributes_valid?
  109 + end
  110 +
  111 + # Returns true or false if the given content type is recognized as an image.
  112 + def image?(content_type)
  113 + content_types.include?(content_type)
  114 + end
  115 +
  116 + # Callback after an image has been resized.
  117 + #
  118 + # class Foo < ActiveRecord::Base
  119 + # acts_as_attachment
  120 + # after_resize do |record, img|
  121 + # record.aspect_ratio = img.columns.to_f / img.rows.to_f
  122 + # end
  123 + # end
  124 + def after_resize(&block)
  125 + write_inheritable_array(:after_resize, [block])
  126 + end
  127 +
  128 + # Callback after an attachment has been saved either to the file system or the DB.
  129 + # Only called if the file has been changed, not necessarily if the record is updated.
  130 + #
  131 + # class Foo < ActiveRecord::Base
  132 + # acts_as_attachment
  133 + # after_attachment_saved do |record|
  134 + # ...
  135 + # end
  136 + # end
  137 + def after_attachment_saved(&block)
  138 + write_inheritable_array(:after_attachment_saved, [block])
  139 + end
  140 +
  141 + # Callback before a thumbnail is saved. Use this to pass any necessary extra attributes that may be required.
  142 + #
  143 + # class Foo < ActiveRecord::Base
  144 + # acts_as_attachment
  145 + # before_thumbnail_saved do |record, thumbnail|
  146 + # ...
  147 + # end
  148 + # end
  149 + def before_thumbnail_saved(&block)
  150 + write_inheritable_array(:before_thumbnail_saved, [block])
  151 + end
  152 +
  153 + # Get the thumbnail class, which is the current attachment class by default.
  154 + # Configure this with the :thumbnail_class option.
  155 + def thumbnail_class
  156 + attachment_options[:thumbnail_class] = attachment_options[:thumbnail_class].constantize unless attachment_options[:thumbnail_class].is_a?(Class)
  157 + attachment_options[:thumbnail_class]
  158 + end
  159 +
  160 + # Copies the given file path to a new tempfile, returning the closed tempfile.
  161 + def copy_to_temp_file(file, temp_base_name)
  162 + returning Tempfile.new(temp_base_name, Technoweenie::AttachmentFu.tempfile_path) do |tmp|
  163 + tmp.close
  164 + FileUtils.cp file, tmp.path
  165 + end
  166 + end
  167 +
  168 + # Writes the given data to a new tempfile, returning the closed tempfile.
  169 + def write_to_temp_file(data, temp_base_name)
  170 + returning Tempfile.new(temp_base_name, Technoweenie::AttachmentFu.tempfile_path) do |tmp|
  171 + tmp.binmode
  172 + tmp.write data
  173 + tmp.close
  174 + end
  175 + end
  176 + end
  177 +
  178 + module InstanceMethods
  179 + # Checks whether the attachment's content type is an image content type
  180 + def image?
  181 + self.class.image?(content_type)
  182 + end
  183 +
  184 + # Returns true/false if an attachment is thumbnailable. A thumbnailable attachment has an image content type and the parent_id attribute.
  185 + def thumbnailable?
  186 + image? && respond_to?(:parent_id) && parent_id.nil?
  187 + end
  188 +
  189 + # Returns the class used to create new thumbnails for this attachment.
  190 + def thumbnail_class
  191 + self.class.thumbnail_class
  192 + end
  193 +
  194 + # Gets the thumbnail name for a filename. 'foo.jpg' becomes 'foo_thumbnail.jpg'
  195 + def thumbnail_name_for(thumbnail = nil)
  196 + return filename if thumbnail.blank?
  197 + ext = nil
  198 + basename = filename.gsub /\.\w+$/ do |s|
  199 + ext = s; ''
  200 + end
  201 + "#{basename}_#{thumbnail}#{ext}"
  202 + end
  203 +
  204 + # Creates or updates the thumbnail for the current attachment.
  205 + def create_or_update_thumbnail(temp_file, file_name_suffix, *size)
  206 + thumbnailable? || raise(ThumbnailError.new("Can't create a thumbnail if the content type is not an image or there is no parent_id column"))
  207 + returning find_or_initialize_thumbnail(file_name_suffix) do |thumb|
  208 + thumb.attributes = {
  209 + :content_type => content_type,
  210 + :filename => thumbnail_name_for(file_name_suffix),
  211 + :temp_path => temp_file,
  212 + :thumbnail_resize_options => size
  213 + }
  214 + callback_with_args :before_thumbnail_saved, thumb
  215 + thumb.save!
  216 + end
  217 + end
  218 +
  219 + # Sets the content type.
  220 + def content_type=(new_type)
  221 + write_attribute :content_type, new_type.to_s.strip
  222 + end
  223 +
  224 + # Sanitizes a filename.
  225 + def filename=(new_name)
  226 + write_attribute :filename, sanitize_filename(new_name)
  227 + end
  228 +
  229 + # Returns the width/height in a suitable format for the image_tag helper: (100x100)
  230 + def image_size
  231 + [width.to_s, height.to_s] * 'x'
  232 + end
  233 +
  234 + # Returns true if the attachment data will be written to the storage system on the next save
  235 + def save_attachment?
  236 + File.file?(temp_path.to_s)
  237 + end
  238 +
  239 + # nil placeholder in case this field is used in a form.
  240 + def uploaded_data() nil; end
  241 +
  242 + # This method handles the uploaded file object. If you set the field name to uploaded_data, you don't need
  243 + # any special code in your controller.
  244 + #
  245 + # <% form_for :attachment, :html => { :multipart => true } do |f| -%>
  246 + # <p><%= f.file_field :uploaded_data %></p>
  247 + # <p><%= submit_tag :Save %>
  248 + # <% end -%>
  249 + #
  250 + # @attachment = Attachment.create! params[:attachment]
  251 + #
  252 + # TODO: Allow it to work with Merb tempfiles too.
  253 + def uploaded_data=(file_data)
  254 + return nil if file_data.nil? || file_data.size == 0
  255 + self.content_type = file_data.content_type
  256 + self.filename = file_data.original_filename if respond_to?(:filename)
  257 + if file_data.is_a?(StringIO)
  258 + file_data.rewind
  259 + self.temp_data = file_data.read
  260 + else
  261 + self.temp_path = file_data.path
  262 + end
  263 + end
  264 +
  265 + # Gets the latest temp path from the collection of temp paths. While working with an attachment,
  266 + # multiple Tempfile objects may be created for various processing purposes (resizing, for example).
  267 + # An array of all the tempfile objects is stored so that the Tempfile instance is held on to until
  268 + # it's not needed anymore. The collection is cleared after saving the attachment.
  269 + def temp_path
  270 + p = temp_paths.first
  271 + p.respond_to?(:path) ? p.path : p.to_s
  272 + end
  273 +
  274 + # Gets an array of the currently used temp paths. Defaults to a copy of #full_filename.
  275 + def temp_paths
  276 + @temp_paths ||= (new_record? || !File.exist?(full_filename)) ? [] : [copy_to_temp_file(full_filename)]
  277 + end
  278 +
  279 + # Adds a new temp_path to the array. This should take a string or a Tempfile. This class makes no
  280 + # attempt to remove the files, so Tempfiles should be used. Tempfiles remove themselves when they go out of scope.
  281 + # You can also use string paths for temporary files, such as those used for uploaded files in a web server.
  282 + def temp_path=(value)
  283 + temp_paths.unshift value
  284 + temp_path
  285 + end
  286 +
  287 + # Gets the data from the latest temp file. This will read the file into memory.
  288 + def temp_data
  289 + save_attachment? ? File.read(temp_path) : nil
  290 + end
  291 +
  292 + # Writes the given data to a Tempfile and adds it to the collection of temp files.
  293 + def temp_data=(data)
  294 + self.temp_path = write_to_temp_file data unless data.nil?
  295 + end
  296 +
  297 + # Copies the given file to a randomly named Tempfile.
  298 + def copy_to_temp_file(file)
  299 + self.class.copy_to_temp_file file, random_tempfile_filename
  300 + end
  301 +
  302 + # Writes the given file to a randomly named Tempfile.
  303 + def write_to_temp_file(data)
  304 + self.class.write_to_temp_file data, random_tempfile_filename
  305 + end
  306 +
  307 + # Stub for creating a temp file from the attachment data. This should be defined in the backend module.
  308 + def create_temp_file() end
  309 +
  310 + # Allows you to work with a processed representation (RMagick, ImageScience, etc) of the attachment in a block.
  311 + #
  312 + # @attachment.with_image do |img|
  313 + # self.data = img.thumbnail(100, 100).to_blob
  314 + # end
  315 + #
  316 + def with_image(&block)
  317 + self.class.with_image(temp_path, &block)
  318 + end
  319 +
  320 + protected
  321 + # Generates a unique filename for a Tempfile.
  322 + def random_tempfile_filename
  323 + "#{rand Time.now.to_i}#{filename || 'attachment'}"
  324 + end
  325 +
  326 + def sanitize_filename(filename)
  327 + returning filename.strip do |name|
  328 + # NOTE: File.basename doesn't work right with Windows paths on Unix
  329 + # get only the filename, not the whole path
  330 + name.gsub! /^.*(\\|\/)/, ''
  331 +
  332 + # Finally, replace all non alphanumeric, underscore or periods with underscore
  333 + name.gsub! /[^\w\.\-]/, '_'
  334 + end
  335 + end
  336 +
  337 + # before_validation callback.
  338 + def set_size_from_temp_path
  339 + self.size = File.size(temp_path) if save_attachment?
  340 + end
  341 +
  342 + # validates the size and content_type attributes according to the current model's options
  343 + def attachment_attributes_valid?
  344 + [:size, :content_type].each do |attr_name|
  345 + enum = attachment_options[attr_name]
  346 + errors.add attr_name, ActiveRecord::Errors.default_error_messages[:inclusion] unless enum.nil? || enum.include?(send(attr_name))
  347 + end
  348 + end
  349 +
  350 + # Initializes a new thumbnail with the given suffix.
  351 + def find_or_initialize_thumbnail(file_name_suffix)
  352 + respond_to?(:parent_id) ?
  353 + thumbnail_class.find_or_initialize_by_thumbnail_and_parent_id(file_name_suffix.to_s, id) :
  354 + thumbnail_class.find_or_initialize_by_thumbnail(file_name_suffix.to_s)
  355 + end
  356 +
  357 + # Stub for a #process_attachment method in a processor
  358 + def process_attachment
  359 + @saved_attachment = save_attachment?
  360 + end
  361 +
  362 + # Cleans up after processing. Thumbnails are created, the attachment is stored to the backend, and the temp_paths are cleared.
  363 + def after_process_attachment
  364 + if @saved_attachment
  365 + if respond_to?(:process_attachment_with_processing) && thumbnailable? && !attachment_options[:thumbnails].blank? && parent_id.nil?
  366 + temp_file = temp_path || create_temp_file
  367 + attachment_options[:thumbnails].each { |suffix, size| create_or_update_thumbnail(temp_file, suffix, *size) }
  368 + end
  369 + save_to_storage
  370 + @temp_paths.clear
  371 + @saved_attachment = nil
  372 + callback :after_attachment_saved
  373 + end
  374 + end
  375 +
  376 + # Resizes the given processed img object with either the attachment resize options or the thumbnail resize options.
  377 + def resize_image_or_thumbnail!(img)
  378 + if (!respond_to?(:parent_id) || parent_id.nil?) && attachment_options[:resize_to] # parent image
  379 + resize_image(img, attachment_options[:resize_to])
  380 + elsif thumbnail_resize_options # thumbnail
  381 + resize_image(img, thumbnail_resize_options)
  382 + end
  383 + end
  384 +
  385 + # Yanked from ActiveRecord::Callbacks, modified so I can pass args to the callbacks besides self.
  386 + # Only accept blocks, however
  387 + def callback_with_args(method, arg = self)
  388 + notify(method)
  389 +
  390 + result = nil
  391 + callbacks_for(method).each do |callback|
  392 + result = callback.call(self, arg)
  393 + return false if result == false
  394 + end
  395 +
  396 + return result
  397 + end
  398 +
  399 + # Removes the thumbnails for the attachment, if it has any
  400 + def destroy_thumbnails
  401 + self.thumbnails.each { |thumbnail| thumbnail.destroy } if thumbnailable?
  402 + end
  403 + end
  404 + end
  405 +end
vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/db_file_backend.rb 0 → 100644
@@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
  1 +module Technoweenie # :nodoc:
  2 + module AttachmentFu # :nodoc:
  3 + module Backends
  4 + # Methods for DB backed attachments
  5 + module DbFileBackend
  6 + def self.included(base) #:nodoc:
  7 + Object.const_set(:DbFile, Class.new(ActiveRecord::Base)) unless Object.const_defined?(:DbFile)
  8 + base.belongs_to :db_file, :class_name => '::DbFile', :foreign_key => 'db_file_id'
  9 + end
  10 +
  11 + # Creates a temp file with the current db data.
  12 + def create_temp_file
  13 + write_to_temp_file current_data
  14 + end
  15 +
  16 + # Gets the current data from the database
  17 + def current_data
  18 + db_file.data
  19 + end
  20 +
  21 + protected
  22 + # Destroys the file. Called in the after_destroy callback
  23 + def destroy_file
  24 + db_file.destroy if db_file
  25 + end
  26 +
  27 + # Saves the data to the DbFile model
  28 + def save_to_storage
  29 + if save_attachment?
  30 + (db_file || build_db_file).data = temp_data
  31 + db_file.save!
  32 + self.class.update_all ['db_file_id = ?', self.db_file_id = db_file.id], ['id = ?', id]
  33 + end
  34 + true
  35 + end
  36 + end
  37 + end
  38 + end
  39 +end
0 \ No newline at end of file 40 \ No newline at end of file
vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/file_system_backend.rb 0 → 100644
@@ -0,0 +1,97 @@ @@ -0,0 +1,97 @@
  1 +require 'ftools'
  2 +module Technoweenie # :nodoc:
  3 + module AttachmentFu # :nodoc:
  4 + module Backends
  5 + # Methods for file system backed attachments
  6 + module FileSystemBackend
  7 + def self.included(base) #:nodoc:
  8 + base.before_update :rename_file
  9 + end
  10 +
  11 + # Gets the full path to the filename in this format:
  12 + #
  13 + # # This assumes a model name like MyModel
  14 + # # public/#{table_name} is the default filesystem path
  15 + # RAILS_ROOT/public/my_models/5/blah.jpg
  16 + #
  17 + # Overwrite this method in your model to customize the filename.
  18 + # The optional thumbnail argument will output the thumbnail's filename.
  19 + def full_filename(thumbnail = nil)
  20 + file_system_path = (thumbnail ? thumbnail_class : self).attachment_options[:path_prefix].to_s
  21 + File.join(RAILS_ROOT, file_system_path, *partitioned_path(thumbnail_name_for(thumbnail)))
  22 + end
  23 +
  24 + # Used as the base path that #public_filename strips off full_filename to create the public path
  25 + def base_path
  26 + @base_path ||= File.join(RAILS_ROOT, 'public')
  27 + end
  28 +
  29 + # The attachment ID used in the full path of a file
  30 + def attachment_path_id
  31 + ((respond_to?(:parent_id) && parent_id) || id).to_i
  32 + end
  33 +
  34 + # overrwrite this to do your own app-specific partitioning.
  35 + # you can thank Jamis Buck for this: http://www.37signals.com/svn/archives2/id_partitioning.php
  36 + def partitioned_path(*args)
  37 + ("%08d" % attachment_path_id).scan(/..../) + args
  38 + end
  39 +
  40 + # Gets the public path to the file
  41 + # The optional thumbnail argument will output the thumbnail's filename.
  42 + def public_filename(thumbnail = nil)
  43 + full_filename(thumbnail).gsub %r(^#{Regexp.escape(base_path)}), ''
  44 + end
  45 +
  46 + def filename=(value)
  47 + @old_filename = full_filename unless filename.nil? || @old_filename
  48 + write_attribute :filename, sanitize_filename(value)
  49 + end
  50 +
  51 + # Creates a temp file from the currently saved file.
  52 + def create_temp_file
  53 + copy_to_temp_file full_filename
  54 + end
  55 +
  56 + protected
  57 + # Destroys the file. Called in the after_destroy callback
  58 + def destroy_file
  59 + FileUtils.rm full_filename
  60 + # remove directory also if it is now empty
  61 + Dir.rmdir(File.dirname(full_filename)) if (Dir.entries(File.dirname(full_filename))-['.','..']).empty?
  62 + rescue
  63 + logger.info "Exception destroying #{full_filename.inspect}: [#{$!.class.name}] #{$1.to_s}"
  64 + logger.warn $!.backtrace.collect { |b| " > #{b}" }.join("\n")
  65 + end
  66 +
  67 + # Renames the given file before saving
  68 + def rename_file
  69 + return unless @old_filename && @old_filename != full_filename
  70 + if save_attachment? && File.exists?(@old_filename)
  71 + FileUtils.rm @old_filename
  72 + elsif File.exists?(@old_filename)
  73 + FileUtils.mv @old_filename, full_filename
  74 + end
  75 + @old_filename = nil
  76 + true
  77 + end
  78 +
  79 + # Saves the file to the file system
  80 + def save_to_storage
  81 + if save_attachment?
  82 + # TODO: This overwrites the file if it exists, maybe have an allow_overwrite option?
  83 + FileUtils.mkdir_p(File.dirname(full_filename))
  84 + File.cp(temp_path, full_filename)
  85 + File.chmod(attachment_options[:chmod] || 0644, full_filename)
  86 + end
  87 + @old_filename = nil
  88 + true
  89 + end
  90 +
  91 + def current_data
  92 + File.file?(full_filename) ? File.read(full_filename) : nil
  93 + end
  94 + end
  95 + end
  96 + end
  97 +end
0 \ No newline at end of file 98 \ No newline at end of file
vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/s3_backend.rb 0 → 100644
@@ -0,0 +1,309 @@ @@ -0,0 +1,309 @@
  1 +module Technoweenie # :nodoc:
  2 + module AttachmentFu # :nodoc:
  3 + module Backends
  4 + # = AWS::S3 Storage Backend
  5 + #
  6 + # Enables use of {Amazon's Simple Storage Service}[http://aws.amazon.com/s3] as a storage mechanism
  7 + #
  8 + # == Requirements
  9 + #
  10 + # Requires the {AWS::S3 Library}[http://amazon.rubyforge.org] for S3 by Marcel Molina Jr. installed either
  11 + # as a gem or a as a Rails plugin.
  12 + #
  13 + # == Configuration
  14 + #
  15 + # Configuration is done via <tt>RAILS_ROOT/config/amazon_s3.yml</tt> and is loaded according to the <tt>RAILS_ENV</tt>.
  16 + # The minimum connection options that you must specify are a bucket name, your access key id and your secret access key.
  17 + # If you don't already have your access keys, all you need to sign up for the S3 service is an account at Amazon.
  18 + # You can sign up for S3 and get access keys by visiting http://aws.amazon.com/s3.
  19 + #
  20 + # Example configuration (RAILS_ROOT/config/amazon_s3.yml)
  21 + #
  22 + # development:
  23 + # bucket_name: appname_development
  24 + # access_key_id: <your key>
  25 + # secret_access_key: <your key>
  26 + #
  27 + # test:
  28 + # bucket_name: appname_test
  29 + # access_key_id: <your key>
  30 + # secret_access_key: <your key>
  31 + #
  32 + # production:
  33 + # bucket_name: appname
  34 + # access_key_id: <your key>
  35 + # secret_access_key: <your key>
  36 + #
  37 + # You can change the location of the config path by passing a full path to the :s3_config_path option.
  38 + #
  39 + # has_attachment :storage => :s3, :s3_config_path => (RAILS_ROOT + '/config/s3.yml')
  40 + #
  41 + # === Required configuration parameters
  42 + #
  43 + # * <tt>:access_key_id</tt> - The access key id for your S3 account. Provided by Amazon.
  44 + # * <tt>:secret_access_key</tt> - The secret access key for your S3 account. Provided by Amazon.
  45 + # * <tt>:bucket_name</tt> - A unique bucket name (think of the bucket_name as being like a database name).
  46 + #
  47 + # If any of these required arguments is missing, a MissingAccessKey exception will be raised from AWS::S3.
  48 + #
  49 + # == About bucket names
  50 + #
  51 + # Bucket names have to be globaly unique across the S3 system. And you can only have up to 100 of them,
  52 + # so it's a good idea to think of a bucket as being like a database, hence the correspondance in this
  53 + # implementation to the development, test, and production environments.
  54 + #
  55 + # The number of objects you can store in a bucket is, for all intents and purposes, unlimited.
  56 + #
  57 + # === Optional configuration parameters
  58 + #
  59 + # * <tt>:server</tt> - The server to make requests to. Defaults to <tt>s3.amazonaws.com</tt>.
  60 + # * <tt>:port</tt> - The port to the requests should be made on. Defaults to 80 or 443 if <tt>:use_ssl</tt> is set.
  61 + # * <tt>:use_ssl</tt> - If set to true, <tt>:port</tt> will be implicitly set to 443, unless specified otherwise. Defaults to false.
  62 + #
  63 + # == Usage
  64 + #
  65 + # To specify S3 as the storage mechanism for a model, set the acts_as_attachment <tt>:storage</tt> option to <tt>:s3</tt>.
  66 + #
  67 + # class Photo < ActiveRecord::Base
  68 + # has_attachment :storage => :s3
  69 + # end
  70 + #
  71 + # === Customizing the path
  72 + #
  73 + # By default, files are prefixed using a pseudo hierarchy in the form of <tt>:table_name/:id</tt>, which results
  74 + # in S3 urls that look like: http(s)://:server/:bucket_name/:table_name/:id/:filename with :table_name
  75 + # representing the customizable portion of the path. You can customize this prefix using the <tt>:path_prefix</tt>
  76 + # option:
  77 + #
  78 + # class Photo < ActiveRecord::Base
  79 + # has_attachment :storage => :s3, :path_prefix => 'my/custom/path'
  80 + # end
  81 + #
  82 + # Which would result in URLs like <tt>http(s)://:server/:bucket_name/my/custom/path/:id/:filename.</tt>
  83 + #
  84 + # === Permissions
  85 + #
  86 + # By default, files are stored on S3 with public access permissions. You can customize this using
  87 + # the <tt>:s3_access</tt> option to <tt>has_attachment</tt>. Available values are
  88 + # <tt>:private</tt>, <tt>:public_read_write</tt>, and <tt>:authenticated_read</tt>.
  89 + #
  90 + # === Other options
  91 + #
  92 + # Of course, all the usual configuration options apply, such as content_type and thumbnails:
  93 + #
  94 + # class Photo < ActiveRecord::Base
  95 + # has_attachment :storage => :s3, :content_type => ['application/pdf', :image], :resize_to => 'x50'
  96 + # has_attachment :storage => :s3, :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
  97 + # end
  98 + #
  99 + # === Accessing S3 URLs
  100 + #
  101 + # You can get an object's URL using the s3_url accessor. For example, assuming that for your postcard app
  102 + # you had a bucket name like 'postcard_world_development', and an attachment model called Photo:
  103 + #
  104 + # @postcard.s3_url # => http(s)://s3.amazonaws.com/postcard_world_development/photos/1/mexico.jpg
  105 + #
  106 + # The resulting url is in the form: http(s)://:server/:bucket_name/:table_name/:id/:file.
  107 + # The optional thumbnail argument will output the thumbnail's filename (if any).
  108 + #
  109 + # Additionally, you can get an object's base path relative to the bucket root using
  110 + # <tt>base_path</tt>:
  111 + #
  112 + # @photo.file_base_path # => photos/1
  113 + #
  114 + # And the full path (including the filename) using <tt>full_filename</tt>:
  115 + #
  116 + # @photo.full_filename # => photos/
  117 + #
  118 + # Niether <tt>base_path</tt> or <tt>full_filename</tt> include the bucket name as part of the path.
  119 + # You can retrieve the bucket name using the <tt>bucket_name</tt> method.
  120 + module S3Backend
  121 + class RequiredLibraryNotFoundError < StandardError; end
  122 + class ConfigFileNotFoundError < StandardError; end
  123 +
  124 + def self.included(base) #:nodoc:
  125 + mattr_reader :bucket_name, :s3_config
  126 +
  127 + begin
  128 + require 'aws/s3'
  129 + include AWS::S3
  130 + rescue LoadError
  131 + raise RequiredLibraryNotFoundError.new('AWS::S3 could not be loaded')
  132 + end
  133 +
  134 + begin
  135 + @@s3_config_path = base.attachment_options[:s3_config_path] || (RAILS_ROOT + '/config/amazon_s3.yml')
  136 + @@s3_config = YAML.load_file(@@s3_config_path)[ENV['RAILS_ENV']].symbolize_keys
  137 + #rescue
  138 + # raise ConfigFileNotFoundError.new('File %s not found' % @@s3_config_path)
  139 + end
  140 +
  141 + @@bucket_name = s3_config[:bucket_name]
  142 +
  143 + Base.establish_connection!(
  144 + :access_key_id => s3_config[:access_key_id],
  145 + :secret_access_key => s3_config[:secret_access_key],
  146 + :server => s3_config[:server],
  147 + :port => s3_config[:port],
  148 + :use_ssl => s3_config[:use_ssl]
  149 + )
  150 +
  151 + # Bucket.create(@@bucket_name)
  152 +
  153 + base.before_update :rename_file
  154 + end
  155 +
  156 + def self.protocol
  157 + @protocol ||= s3_config[:use_ssl] ? 'https://' : 'http://'
  158 + end
  159 +
  160 + def self.hostname
  161 + @hostname ||= s3_config[:server] || AWS::S3::DEFAULT_HOST
  162 + end
  163 +
  164 + def self.port_string
  165 + @port_string ||= s3_config[:port] == (s3_config[:use_ssl] ? 443 : 80) ? '' : ":#{s3_config[:port]}"
  166 + end
  167 +
  168 + module ClassMethods
  169 + def s3_protocol
  170 + Technoweenie::AttachmentFu::Backends::S3Backend.protocol
  171 + end
  172 +
  173 + def s3_hostname
  174 + Technoweenie::AttachmentFu::Backends::S3Backend.hostname
  175 + end
  176 +
  177 + def s3_port_string
  178 + Technoweenie::AttachmentFu::Backends::S3Backend.port_string
  179 + end
  180 + end
  181 +
  182 + # Overwrites the base filename writer in order to store the old filename
  183 + def filename=(value)
  184 + @old_filename = filename unless filename.nil? || @old_filename
  185 + write_attribute :filename, sanitize_filename(value)
  186 + end
  187 +
  188 + # The attachment ID used in the full path of a file
  189 + def attachment_path_id
  190 + ((respond_to?(:parent_id) && parent_id) || id).to_s
  191 + end
  192 +
  193 + # The pseudo hierarchy containing the file relative to the bucket name
  194 + # Example: <tt>:table_name/:id</tt>
  195 + def base_path
  196 + File.join(attachment_options[:path_prefix], attachment_path_id)
  197 + end
  198 +
  199 + # The full path to the file relative to the bucket name
  200 + # Example: <tt>:table_name/:id/:filename</tt>
  201 + def full_filename(thumbnail = nil)
  202 + File.join(base_path, thumbnail_name_for(thumbnail))
  203 + end
  204 +
  205 + # All public objects are accessible via a GET request to the S3 servers. You can generate a
  206 + # url for an object using the s3_url method.
  207 + #
  208 + # @photo.s3_url
  209 + #
  210 + # The resulting url is in the form: <tt>http(s)://:server/:bucket_name/:table_name/:id/:file</tt> where
  211 + # the <tt>:server</tt> variable defaults to <tt>AWS::S3 URL::DEFAULT_HOST</tt> (s3.amazonaws.com) and can be
  212 + # set using the configuration parameters in <tt>RAILS_ROOT/config/amazon_s3.yml</tt>.
  213 + #
  214 + # The optional thumbnail argument will output the thumbnail's filename (if any).
  215 + def s3_url(thumbnail = nil)
  216 + File.join(s3_protocol + s3_hostname + s3_port_string, bucket_name, full_filename(thumbnail))
  217 + end
  218 + alias :public_filename :s3_url
  219 +
  220 + # All private objects are accessible via an authenticated GET request to the S3 servers. You can generate an
  221 + # authenticated url for an object like this:
  222 + #
  223 + # @photo.authenticated_s3_url
  224 + #
  225 + # By default authenticated urls expire 5 minutes after they were generated.
  226 + #
  227 + # Expiration options can be specified either with an absolute time using the <tt>:expires</tt> option,
  228 + # or with a number of seconds relative to now with the <tt>:expires_in</tt> option:
  229 + #
  230 + # # Absolute expiration date (October 13th, 2025)
  231 + # @photo.authenticated_s3_url(:expires => Time.mktime(2025,10,13).to_i)
  232 + #
  233 + # # Expiration in five hours from now
  234 + # @photo.authenticated_s3_url(:expires_in => 5.hours)
  235 + #
  236 + # You can specify whether the url should go over SSL with the <tt>:use_ssl</tt> option.
  237 + # By default, the ssl settings for the current connection will be used:
  238 + #
  239 + # @photo.authenticated_s3_url(:use_ssl => true)
  240 + #
  241 + # Finally, the optional thumbnail argument will output the thumbnail's filename (if any):
  242 + #
  243 + # @photo.authenticated_s3_url('thumbnail', :expires_in => 5.hours, :use_ssl => true)
  244 + def authenticated_s3_url(*args)
  245 + thumbnail = args.first.is_a?(String) ? args.first : nil
  246 + options = args.last.is_a?(Hash) ? args.last : {}
  247 + S3Object.url_for(full_filename(thumbnail), bucket_name, options)
  248 + end
  249 +
  250 + def create_temp_file
  251 + write_to_temp_file current_data
  252 + end
  253 +
  254 + def current_data
  255 + S3Object.value full_filename, bucket_name
  256 + end
  257 +
  258 + def s3_protocol
  259 + Technoweenie::AttachmentFu::Backends::S3Backend.protocol
  260 + end
  261 +
  262 + def s3_hostname
  263 + Technoweenie::AttachmentFu::Backends::S3Backend.hostname
  264 + end
  265 +
  266 + def s3_port_string
  267 + Technoweenie::AttachmentFu::Backends::S3Backend.port_string
  268 + end
  269 +
  270 + protected
  271 + # Called in the after_destroy callback
  272 + def destroy_file
  273 + S3Object.delete full_filename, bucket_name
  274 + end
  275 +
  276 + def rename_file
  277 + return unless @old_filename && @old_filename != filename
  278 +
  279 + old_full_filename = File.join(base_path, @old_filename)
  280 +
  281 + S3Object.rename(
  282 + old_full_filename,
  283 + full_filename,
  284 + bucket_name,
  285 + :access => attachment_options[:s3_access]
  286 + )
  287 +
  288 + @old_filename = nil
  289 + true
  290 + end
  291 +
  292 + def save_to_storage
  293 + if save_attachment?
  294 + S3Object.store(
  295 + full_filename,
  296 + (temp_path ? File.open(temp_path) : temp_data),
  297 + bucket_name,
  298 + :content_type => content_type,
  299 + :access => attachment_options[:s3_access]
  300 + )
  301 + end
  302 +
  303 + @old_filename = nil
  304 + true
  305 + end
  306 + end
  307 + end
  308 + end
  309 +end
0 \ No newline at end of file 310 \ No newline at end of file
vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/image_science_processor.rb 0 → 100644
@@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
  1 +require 'image_science'
  2 +module Technoweenie # :nodoc:
  3 + module AttachmentFu # :nodoc:
  4 + module Processors
  5 + module ImageScienceProcessor
  6 + def self.included(base)
  7 + base.send :extend, ClassMethods
  8 + base.alias_method_chain :process_attachment, :processing
  9 + end
  10 +
  11 + module ClassMethods
  12 + # Yields a block containing an RMagick Image for the given binary data.
  13 + def with_image(file, &block)
  14 + ::ImageScience.with_image file, &block
  15 + end
  16 + end
  17 +
  18 + protected
  19 + def process_attachment_with_processing
  20 + return unless process_attachment_without_processing && image?
  21 + with_image do |img|
  22 + self.width = img.width if respond_to?(:width)
  23 + self.height = img.height if respond_to?(:height)
  24 + resize_image_or_thumbnail! img
  25 + end
  26 + end
  27 +
  28 + # Performs the actual resizing operation for a thumbnail
  29 + def resize_image(img, size)
  30 + # create a dummy temp file to write to
  31 + filename.sub! /gif$/, 'png'
  32 + self.temp_path = write_to_temp_file(filename)
  33 + grab_dimensions = lambda do |img|
  34 + self.width = img.width if respond_to?(:width)
  35 + self.height = img.height if respond_to?(:height)
  36 + img.save temp_path
  37 + callback_with_args :after_resize, img
  38 + end
  39 +
  40 + size = size.first if size.is_a?(Array) && size.length == 1
  41 + if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum))
  42 + if size.is_a?(Fixnum)
  43 + img.thumbnail(size, &grab_dimensions)
  44 + else
  45 + img.resize(size[0], size[1], &grab_dimensions)
  46 + end
  47 + else
  48 + new_size = [img.width, img.height] / size.to_s
  49 + img.resize(new_size[0], new_size[1], &grab_dimensions)
  50 + end
  51 + end
  52 + end
  53 + end
  54 + end
  55 +end
0 \ No newline at end of file 56 \ No newline at end of file
vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/mini_magick_processor.rb 0 → 100644
@@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
  1 +require 'mini_magick'
  2 +module Technoweenie # :nodoc:
  3 + module AttachmentFu # :nodoc:
  4 + module Processors
  5 + module MiniMagickProcessor
  6 + def self.included(base)
  7 + base.send :extend, ClassMethods
  8 + base.alias_method_chain :process_attachment, :processing
  9 + end
  10 +
  11 + module ClassMethods
  12 + # Yields a block containing an MiniMagick Image for the given binary data.
  13 + def with_image(file, &block)
  14 + begin
  15 + binary_data = file.is_a?(MiniMagick::Image) ? file : MiniMagick::Image.from_file(file) unless !Object.const_defined?(:MiniMagick)
  16 + rescue
  17 + # Log the failure to load the image.
  18 + logger.debug("Exception working with image: #{$!}")
  19 + binary_data = nil
  20 + end
  21 + block.call binary_data if block && binary_data
  22 + ensure
  23 + !binary_data.nil?
  24 + end
  25 + end
  26 +
  27 + protected
  28 + def process_attachment_with_processing
  29 + return unless process_attachment_without_processing
  30 + with_image do |img|
  31 + resize_image_or_thumbnail! img
  32 + self.width = img[:width] if respond_to?(:width)
  33 + self.height = img[:height] if respond_to?(:height)
  34 + callback_with_args :after_resize, img
  35 + end if image?
  36 + end
  37 +
  38 + # Performs the actual resizing operation for a thumbnail
  39 + def resize_image(img, size)
  40 + size = size.first if size.is_a?(Array) && size.length == 1
  41 + if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum))
  42 + if size.is_a?(Fixnum)
  43 + size = [size, size]
  44 + img.resize(size.join('x'))
  45 + else
  46 + img.resize(size.join('x') + '!')
  47 + end
  48 + else
  49 + img.resize(size.to_s)
  50 + end
  51 + self.temp_path = img
  52 + end
  53 + end
  54 + end
  55 + end
  56 +end
0 \ No newline at end of file 57 \ No newline at end of file
vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/rmagick_processor.rb 0 → 100644
@@ -0,0 +1,53 @@ @@ -0,0 +1,53 @@
  1 +require 'RMagick'
  2 +module Technoweenie # :nodoc:
  3 + module AttachmentFu # :nodoc:
  4 + module Processors
  5 + module RmagickProcessor
  6 + def self.included(base)
  7 + base.send :extend, ClassMethods
  8 + base.alias_method_chain :process_attachment, :processing
  9 + end
  10 +
  11 + module ClassMethods
  12 + # Yields a block containing an RMagick Image for the given binary data.
  13 + def with_image(file, &block)
  14 + begin
  15 + binary_data = file.is_a?(Magick::Image) ? file : Magick::Image.read(file).first unless !Object.const_defined?(:Magick)
  16 + rescue
  17 + # Log the failure to load the image. This should match ::Magick::ImageMagickError
  18 + # but that would cause acts_as_attachment to require rmagick.
  19 + logger.debug("Exception working with image: #{$!}")
  20 + binary_data = nil
  21 + end
  22 + block.call binary_data if block && binary_data
  23 + ensure
  24 + !binary_data.nil?
  25 + end
  26 + end
  27 +
  28 + protected
  29 + def process_attachment_with_processing
  30 + return unless process_attachment_without_processing
  31 + with_image do |img|
  32 + resize_image_or_thumbnail! img
  33 + self.width = img.columns if respond_to?(:width)
  34 + self.height = img.rows if respond_to?(:height)
  35 + callback_with_args :after_resize, img
  36 + end if image?
  37 + end
  38 +
  39 + # Performs the actual resizing operation for a thumbnail
  40 + def resize_image(img, size)
  41 + size = size.first if size.is_a?(Array) && size.length == 1 && !size.first.is_a?(Fixnum)
  42 + if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum))
  43 + size = [size, size] if size.is_a?(Fixnum)
  44 + img.thumbnail!(*size)
  45 + else
  46 + img.change_geometry(size.to_s) { |cols, rows, image| image.resize!(cols, rows) }
  47 + end
  48 + self.temp_path = write_to_temp_file(img.to_blob)
  49 + end
  50 + end
  51 + end
  52 + end
  53 +end
0 \ No newline at end of file 54 \ No newline at end of file
vendor/plugins/attachment_fu/test/amazon_s3.yml 0 → 100644
@@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
  1 +test:
  2 + bucket_name: afu
  3 + access_key_id: YOURACCESSKEY
  4 + secret_access_key: YOURSECRETACCESSKEY
  5 + server: 127.0.0.1
  6 + port: 3002
0 \ No newline at end of file 7 \ No newline at end of file
vendor/plugins/attachment_fu/test/backends/db_file_test.rb 0 → 100644
@@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
  1 +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper'))
  2 +
  3 +class DbFileTest < Test::Unit::TestCase
  4 + include BaseAttachmentTests
  5 + attachment_model Attachment
  6 +
  7 + def test_should_call_after_attachment_saved(klass = Attachment)
  8 + attachment_model.saves = 0
  9 + assert_created do
  10 + upload_file :filename => '/files/rails.png'
  11 + end
  12 + assert_equal 1, attachment_model.saves
  13 + end
  14 +
  15 + test_against_subclass :test_should_call_after_attachment_saved, Attachment
  16 +end
0 \ No newline at end of file 17 \ No newline at end of file
vendor/plugins/attachment_fu/test/backends/file_system_test.rb 0 → 100644
@@ -0,0 +1,80 @@ @@ -0,0 +1,80 @@
  1 +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper'))
  2 +
  3 +class FileSystemTest < Test::Unit::TestCase
  4 + include BaseAttachmentTests
  5 + attachment_model FileAttachment
  6 +
  7 + def test_filesystem_size_for_file_attachment(klass = FileAttachment)
  8 + attachment_model klass
  9 + assert_created 1 do
  10 + attachment = upload_file :filename => '/files/rails.png'
  11 + assert_equal attachment.size, File.open(attachment.full_filename).stat.size
  12 + end
  13 + end
  14 +
  15 + test_against_subclass :test_filesystem_size_for_file_attachment, FileAttachment
  16 +
  17 + def test_should_not_overwrite_file_attachment(klass = FileAttachment)
  18 + attachment_model klass
  19 + assert_created 2 do
  20 + real = upload_file :filename => '/files/rails.png'
  21 + assert_valid real
  22 + assert !real.new_record?, real.errors.full_messages.join("\n")
  23 + assert !real.size.zero?
  24 +
  25 + fake = upload_file :filename => '/files/fake/rails.png'
  26 + assert_valid fake
  27 + assert !fake.size.zero?
  28 +
  29 + assert_not_equal File.open(real.full_filename).stat.size, File.open(fake.full_filename).stat.size
  30 + end
  31 + end
  32 +
  33 + test_against_subclass :test_should_not_overwrite_file_attachment, FileAttachment
  34 +
  35 + def test_should_store_file_attachment_in_filesystem(klass = FileAttachment)
  36 + attachment_model klass
  37 + attachment = nil
  38 + assert_created do
  39 + attachment = upload_file :filename => '/files/rails.png'
  40 + assert_valid attachment
  41 + assert File.exists?(attachment.full_filename), "#{attachment.full_filename} does not exist"
  42 + end
  43 + attachment
  44 + end
  45 +
  46 + test_against_subclass :test_should_store_file_attachment_in_filesystem, FileAttachment
  47 +
  48 + def test_should_delete_old_file_when_updating(klass = FileAttachment)
  49 + attachment_model klass
  50 + attachment = upload_file :filename => '/files/rails.png'
  51 + old_filename = attachment.full_filename
  52 + assert_not_created do
  53 + use_temp_file 'files/rails.png' do |file|
  54 + attachment.filename = 'rails2.png'
  55 + attachment.temp_path = File.join(fixture_path, file)
  56 + attachment.save!
  57 + assert File.exists?(attachment.full_filename), "#{attachment.full_filename} does not exist"
  58 + assert !File.exists?(old_filename), "#{old_filename} still exists"
  59 + end
  60 + end
  61 + end
  62 +
  63 + test_against_subclass :test_should_delete_old_file_when_updating, FileAttachment
  64 +
  65 + def test_should_delete_old_file_when_renaming(klass = FileAttachment)
  66 + attachment_model klass
  67 + attachment = upload_file :filename => '/files/rails.png'
  68 + old_filename = attachment.full_filename
  69 + assert_not_created do
  70 + attachment.filename = 'rails2.png'
  71 + attachment.save
  72 + assert File.exists?(attachment.full_filename), "#{attachment.full_filename} does not exist"
  73 + assert !File.exists?(old_filename), "#{old_filename} still exists"
  74 + assert !attachment.reload.size.zero?
  75 + assert_equal 'rails2.png', attachment.filename
  76 + end
  77 + end
  78 +
  79 + test_against_subclass :test_should_delete_old_file_when_renaming, FileAttachment
  80 +end
0 \ No newline at end of file 81 \ No newline at end of file
vendor/plugins/attachment_fu/test/backends/remote/s3_test.rb 0 → 100644
@@ -0,0 +1,103 @@ @@ -0,0 +1,103 @@
  1 +require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'test_helper'))
  2 +require 'net/http'
  3 +
  4 +class S3Test < Test::Unit::TestCase
  5 + if File.exist?(File.join(File.dirname(__FILE__), '../../amazon_s3.yml'))
  6 + include BaseAttachmentTests
  7 + attachment_model S3Attachment
  8 +
  9 + def test_should_create_correct_bucket_name(klass = S3Attachment)
  10 + attachment_model klass
  11 + attachment = upload_file :filename => '/files/rails.png'
  12 + assert_equal attachment.s3_config[:bucket_name], attachment.bucket_name
  13 + end
  14 +
  15 + test_against_subclass :test_should_create_correct_bucket_name, S3Attachment
  16 +
  17 + def test_should_create_default_path_prefix(klass = S3Attachment)
  18 + attachment_model klass
  19 + attachment = upload_file :filename => '/files/rails.png'
  20 + assert_equal File.join(attachment_model.table_name, attachment.attachment_path_id), attachment.base_path
  21 + end
  22 +
  23 + test_against_subclass :test_should_create_default_path_prefix, S3Attachment
  24 +
  25 + def test_should_create_custom_path_prefix(klass = S3WithPathPrefixAttachment)
  26 + attachment_model klass
  27 + attachment = upload_file :filename => '/files/rails.png'
  28 + assert_equal File.join('some/custom/path/prefix', attachment.attachment_path_id), attachment.base_path
  29 + end
  30 +
  31 + test_against_subclass :test_should_create_custom_path_prefix, S3WithPathPrefixAttachment
  32 +
  33 + def test_should_create_valid_url(klass = S3Attachment)
  34 + attachment_model klass
  35 + attachment = upload_file :filename => '/files/rails.png'
  36 + assert_equal "#{s3_protocol}#{s3_hostname}#{s3_port_string}/#{attachment.bucket_name}/#{attachment.full_filename}", attachment.s3_url
  37 + end
  38 +
  39 + test_against_subclass :test_should_create_valid_url, S3Attachment
  40 +
  41 + def test_should_create_authenticated_url(klass = S3Attachment)
  42 + attachment_model klass
  43 + attachment = upload_file :filename => '/files/rails.png'
  44 + assert_match /^http.+AWSAccessKeyId.+Expires.+Signature.+/, attachment.authenticated_s3_url(:use_ssl => true)
  45 + end
  46 +
  47 + test_against_subclass :test_should_create_authenticated_url, S3Attachment
  48 +
  49 + def test_should_save_attachment(klass = S3Attachment)
  50 + attachment_model klass
  51 + assert_created do
  52 + attachment = upload_file :filename => '/files/rails.png'
  53 + assert_valid attachment
  54 + assert attachment.image?
  55 + assert !attachment.size.zero?
  56 + assert_kind_of Net::HTTPOK, http_response_for(attachment.s3_url)
  57 + end
  58 + end
  59 +
  60 + test_against_subclass :test_should_save_attachment, S3Attachment
  61 +
  62 + def test_should_delete_attachment_from_s3_when_attachment_record_destroyed(klass = S3Attachment)
  63 + attachment_model klass
  64 + attachment = upload_file :filename => '/files/rails.png'
  65 +
  66 + urls = [attachment.s3_url] + attachment.thumbnails.collect(&:s3_url)
  67 +
  68 + urls.each {|url| assert_kind_of Net::HTTPOK, http_response_for(url) }
  69 + attachment.destroy
  70 + urls.each do |url|
  71 + begin
  72 + http_response_for(url)
  73 + rescue Net::HTTPForbidden, Net::HTTPNotFound
  74 + nil
  75 + end
  76 + end
  77 + end
  78 +
  79 + test_against_subclass :test_should_delete_attachment_from_s3_when_attachment_record_destroyed, S3Attachment
  80 +
  81 + protected
  82 + def http_response_for(url)
  83 + url = URI.parse(url)
  84 + Net::HTTP.start(url.host, url.port) {|http| http.request_head(url.path) }
  85 + end
  86 +
  87 + def s3_protocol
  88 + Technoweenie::AttachmentFu::Backends::S3Backend.protocol
  89 + end
  90 +
  91 + def s3_hostname
  92 + Technoweenie::AttachmentFu::Backends::S3Backend.hostname
  93 + end
  94 +
  95 + def s3_port_string
  96 + Technoweenie::AttachmentFu::Backends::S3Backend.port_string
  97 + end
  98 + else
  99 + def test_flunk_s3
  100 + puts "s3 config file not loaded, tests not running"
  101 + end
  102 + end
  103 +end
0 \ No newline at end of file 104 \ No newline at end of file
vendor/plugins/attachment_fu/test/base_attachment_tests.rb 0 → 100644
@@ -0,0 +1,57 @@ @@ -0,0 +1,57 @@
  1 +module BaseAttachmentTests
  2 + def test_should_create_file_from_uploaded_file
  3 + assert_created do
  4 + attachment = upload_file :filename => '/files/foo.txt'
  5 + assert_valid attachment
  6 + assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file)
  7 + assert attachment.image?
  8 + assert !attachment.size.zero?
  9 + #assert_equal 3, attachment.size
  10 + assert_nil attachment.width
  11 + assert_nil attachment.height
  12 + end
  13 + end
  14 +
  15 + def test_reassign_attribute_data
  16 + assert_created 1 do
  17 + attachment = upload_file :filename => '/files/rails.png'
  18 + assert_valid attachment
  19 + assert attachment.size > 0, "no data was set"
  20 +
  21 + attachment.temp_data = 'wtf'
  22 + assert attachment.save_attachment?
  23 + attachment.save!
  24 +
  25 + assert_equal 'wtf', attachment_model.find(attachment.id).send(:current_data)
  26 + end
  27 + end
  28 +
  29 + def test_no_reassign_attribute_data_on_nil
  30 + assert_created 1 do
  31 + attachment = upload_file :filename => '/files/rails.png'
  32 + assert_valid attachment
  33 + assert attachment.size > 0, "no data was set"
  34 +
  35 + attachment.temp_data = nil
  36 + assert !attachment.save_attachment?
  37 + end
  38 + end
  39 +
  40 + def test_should_overwrite_old_contents_when_updating
  41 + attachment = upload_file :filename => '/files/rails.png'
  42 + assert_not_created do # no new db_file records
  43 + use_temp_file 'files/rails.png' do |file|
  44 + attachment.filename = 'rails2.png'
  45 + attachment.temp_path = File.join(fixture_path, file)
  46 + attachment.save!
  47 + end
  48 + end
  49 + end
  50 +
  51 + def test_should_save_without_updating_file
  52 + attachment = upload_file :filename => '/files/foo.txt'
  53 + assert_valid attachment
  54 + assert !attachment.save_attachment?
  55 + assert_nothing_raised { attachment.save! }
  56 + end
  57 +end
0 \ No newline at end of file 58 \ No newline at end of file
vendor/plugins/attachment_fu/test/basic_test.rb 0 → 100644
@@ -0,0 +1,64 @@ @@ -0,0 +1,64 @@
  1 +require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
  2 +
  3 +class BasicTest < Test::Unit::TestCase
  4 + def test_should_set_default_min_size
  5 + assert_equal 1, Attachment.attachment_options[:min_size]
  6 + end
  7 +
  8 + def test_should_set_default_max_size
  9 + assert_equal 1.megabyte, Attachment.attachment_options[:max_size]
  10 + end
  11 +
  12 + def test_should_set_default_size
  13 + assert_equal (1..1.megabyte), Attachment.attachment_options[:size]
  14 + end
  15 +
  16 + def test_should_set_default_thumbnails_option
  17 + assert_equal Hash.new, Attachment.attachment_options[:thumbnails]
  18 + end
  19 +
  20 + def test_should_set_default_thumbnail_class
  21 + assert_equal Attachment, Attachment.attachment_options[:thumbnail_class]
  22 + end
  23 +
  24 + def test_should_normalize_content_types_to_array
  25 + assert_equal %w(pdf), PdfAttachment.attachment_options[:content_type]
  26 + assert_equal %w(pdf doc txt), DocAttachment.attachment_options[:content_type]
  27 + assert_equal ['image/jpeg', 'image/pjpeg', 'image/gif', 'image/png', 'image/x-png'], ImageAttachment.attachment_options[:content_type]
  28 + assert_equal ['pdf', 'image/jpeg', 'image/pjpeg', 'image/gif', 'image/png', 'image/x-png'], ImageOrPdfAttachment.attachment_options[:content_type]
  29 + end
  30 +
  31 + def test_should_sanitize_content_type
  32 + @attachment = Attachment.new :content_type => ' foo '
  33 + assert_equal 'foo', @attachment.content_type
  34 + end
  35 +
  36 + def test_should_sanitize_filenames
  37 + @attachment = Attachment.new :filename => 'blah/foo.bar'
  38 + assert_equal 'foo.bar', @attachment.filename
  39 +
  40 + @attachment.filename = 'blah\\foo.bar'
  41 + assert_equal 'foo.bar', @attachment.filename
  42 +
  43 + @attachment.filename = 'f o!O-.bar'
  44 + assert_equal 'f_o_O-.bar', @attachment.filename
  45 + end
  46 +
  47 + def test_should_convert_thumbnail_name
  48 + @attachment = FileAttachment.new :filename => 'foo.bar'
  49 + assert_equal 'foo.bar', @attachment.thumbnail_name_for(nil)
  50 + assert_equal 'foo.bar', @attachment.thumbnail_name_for('')
  51 + assert_equal 'foo_blah.bar', @attachment.thumbnail_name_for(:blah)
  52 + assert_equal 'foo_blah.blah.bar', @attachment.thumbnail_name_for('blah.blah')
  53 +
  54 + @attachment.filename = 'foo.bar.baz'
  55 + assert_equal 'foo.bar_blah.baz', @attachment.thumbnail_name_for(:blah)
  56 + end
  57 +
  58 + def test_should_require_valid_thumbnails_option
  59 + klass = Class.new(ActiveRecord::Base)
  60 + assert_raise ArgumentError do
  61 + klass.has_attachment :thumbnails => []
  62 + end
  63 + end
  64 +end
0 \ No newline at end of file 65 \ No newline at end of file
vendor/plugins/attachment_fu/test/database.yml 0 → 100644
@@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
  1 +sqlite:
  2 + :adapter: sqlite
  3 + :dbfile: attachment_fu_plugin.sqlite.db
  4 +sqlite3:
  5 + :adapter: sqlite3
  6 + :dbfile: attachment_fu_plugin.sqlite3.db
  7 +postgresql:
  8 + :adapter: postgresql
  9 + :username: postgres
  10 + :password: postgres
  11 + :database: attachment_fu_plugin_test
  12 + :min_messages: ERROR
  13 +mysql:
  14 + :adapter: mysql
  15 + :host: localhost
  16 + :username: rails
  17 + :password:
  18 + :database: attachment_fu_plugin_test
0 \ No newline at end of file 19 \ No newline at end of file
vendor/plugins/attachment_fu/test/extra_attachment_test.rb 0 → 100644
@@ -0,0 +1,57 @@ @@ -0,0 +1,57 @@
  1 +require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
  2 +
  3 +class OrphanAttachmentTest < Test::Unit::TestCase
  4 + include BaseAttachmentTests
  5 + attachment_model OrphanAttachment
  6 +
  7 + def test_should_create_image_from_uploaded_file
  8 + assert_created do
  9 + attachment = upload_file :filename => '/files/rails.png'
  10 + assert_valid attachment
  11 + assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file)
  12 + assert attachment.image?
  13 + assert !attachment.size.zero?
  14 + end
  15 + end
  16 +
  17 + def test_should_create_file_from_uploaded_file
  18 + assert_created do
  19 + attachment = upload_file :filename => '/files/foo.txt'
  20 + assert_valid attachment
  21 + assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file)
  22 + assert attachment.image?
  23 + assert !attachment.size.zero?
  24 + end
  25 + end
  26 +
  27 + def test_should_create_image_from_uploaded_file_with_custom_content_type
  28 + assert_created do
  29 + attachment = upload_file :content_type => 'foo/bar', :filename => '/files/rails.png'
  30 + assert_valid attachment
  31 + assert !attachment.image?
  32 + assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file)
  33 + assert !attachment.size.zero?
  34 + #assert_equal 1784, attachment.size
  35 + end
  36 + end
  37 +
  38 + def test_should_create_thumbnail
  39 + attachment = upload_file :filename => '/files/rails.png'
  40 +
  41 + assert_raise Technoweenie::AttachmentFu::ThumbnailError do
  42 + attachment.create_or_update_thumbnail(attachment.create_temp_file, 'thumb', 50, 50)
  43 + end
  44 + end
  45 +
  46 + def test_should_create_thumbnail_with_geometry_string
  47 + attachment = upload_file :filename => '/files/rails.png'
  48 +
  49 + assert_raise Technoweenie::AttachmentFu::ThumbnailError do
  50 + attachment.create_or_update_thumbnail(attachment.create_temp_file, 'thumb', 'x50')
  51 + end
  52 + end
  53 +end
  54 +
  55 +class MinimalAttachmentTest < OrphanAttachmentTest
  56 + attachment_model MinimalAttachment
  57 +end
0 \ No newline at end of file 58 \ No newline at end of file
vendor/plugins/attachment_fu/test/fixtures/attachment.rb 0 → 100644
@@ -0,0 +1,127 @@ @@ -0,0 +1,127 @@
  1 +class Attachment < ActiveRecord::Base
  2 + @@saves = 0
  3 + cattr_accessor :saves
  4 + has_attachment :processor => :rmagick
  5 + validates_as_attachment
  6 + after_attachment_saved do |record|
  7 + self.saves += 1
  8 + end
  9 +end
  10 +
  11 +class SmallAttachment < Attachment
  12 + has_attachment :max_size => 1.kilobyte
  13 +end
  14 +
  15 +class BigAttachment < Attachment
  16 + has_attachment :size => 1.megabyte..2.megabytes
  17 +end
  18 +
  19 +class PdfAttachment < Attachment
  20 + has_attachment :content_type => 'pdf'
  21 +end
  22 +
  23 +class DocAttachment < Attachment
  24 + has_attachment :content_type => %w(pdf doc txt)
  25 +end
  26 +
  27 +class ImageAttachment < Attachment
  28 + has_attachment :content_type => :image, :resize_to => [50,50]
  29 +end
  30 +
  31 +class ImageOrPdfAttachment < Attachment
  32 + has_attachment :content_type => ['pdf', :image], :resize_to => 'x50'
  33 +end
  34 +
  35 +class ImageWithThumbsAttachment < Attachment
  36 + has_attachment :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }, :resize_to => [55,55]
  37 + after_resize do |record, img|
  38 + record.aspect_ratio = img.columns.to_f / img.rows.to_f
  39 + end
  40 +end
  41 +
  42 +class FileAttachment < ActiveRecord::Base
  43 + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files', :processor => :rmagick
  44 + validates_as_attachment
  45 +end
  46 +
  47 +class ImageFileAttachment < FileAttachment
  48 + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files',
  49 + :content_type => :image, :resize_to => [50,50]
  50 +end
  51 +
  52 +class ImageWithThumbsFileAttachment < FileAttachment
  53 + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files',
  54 + :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }, :resize_to => [55,55]
  55 + after_resize do |record, img|
  56 + record.aspect_ratio = img.columns.to_f / img.rows.to_f
  57 + end
  58 +end
  59 +
  60 +class ImageWithThumbsClassFileAttachment < FileAttachment
  61 + # use file_system_path to test backwards compatibility
  62 + has_attachment :file_system_path => 'vendor/plugins/attachment_fu/test/files',
  63 + :thumbnails => { :thumb => [50, 50] }, :resize_to => [55,55],
  64 + :thumbnail_class => 'ImageThumbnail'
  65 +end
  66 +
  67 +class ImageThumbnail < FileAttachment
  68 + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files/thumbnails'
  69 +end
  70 +
  71 +# no parent
  72 +class OrphanAttachment < ActiveRecord::Base
  73 + has_attachment :processor => :rmagick
  74 + validates_as_attachment
  75 +end
  76 +
  77 +# no filename, no size, no content_type
  78 +class MinimalAttachment < ActiveRecord::Base
  79 + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files', :processor => :rmagick
  80 + validates_as_attachment
  81 +
  82 + def filename
  83 + "#{id}.file"
  84 + end
  85 +end
  86 +
  87 +begin
  88 + class ImageScienceAttachment < ActiveRecord::Base
  89 + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files',
  90 + :processor => :image_science, :thumbnails => { :thumb => [50, 51], :geometry => '31>' }, :resize_to => 55
  91 + end
  92 +rescue MissingSourceFile
  93 + puts $!.message
  94 + puts "no ImageScience"
  95 +end
  96 +
  97 +begin
  98 + class MiniMagickAttachment < ActiveRecord::Base
  99 + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files',
  100 + :processor => :mini_magick, :thumbnails => { :thumb => [50, 51], :geometry => '31>' }, :resize_to => 55
  101 + end
  102 +rescue MissingSourceFile
  103 + puts $!.message
  104 + puts "no Mini Magick"
  105 +end
  106 +
  107 +begin
  108 + class MiniMagickAttachment < ActiveRecord::Base
  109 + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files',
  110 + :processor => :mini_magick, :thumbnails => { :thumb => [50, 51], :geometry => '31>' }, :resize_to => 55
  111 + end
  112 +rescue MissingSourceFile
  113 +end
  114 +
  115 +begin
  116 + class S3Attachment < ActiveRecord::Base
  117 + has_attachment :storage => :s3, :processor => :rmagick, :s3_config_path => File.join(File.dirname(__FILE__), '../amazon_s3.yml')
  118 + validates_as_attachment
  119 + end
  120 +
  121 + class S3WithPathPrefixAttachment < S3Attachment
  122 + has_attachment :storage => :s3, :path_prefix => 'some/custom/path/prefix', :processor => :rmagick
  123 + validates_as_attachment
  124 + end
  125 +rescue Technoweenie::AttachmentFu::Backends::S3Backend::ConfigFileNotFoundError
  126 + puts "S3 error: #{$!}"
  127 +end
vendor/plugins/attachment_fu/test/fixtures/files/fake/rails.png 0 → 100644

4.12 KB

vendor/plugins/attachment_fu/test/fixtures/files/foo.txt 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +foo
0 \ No newline at end of file 2 \ No newline at end of file
vendor/plugins/attachment_fu/test/fixtures/files/rails.png 0 → 100644

1.75 KB

vendor/plugins/attachment_fu/test/geometry_test.rb 0 → 100644
@@ -0,0 +1,101 @@ @@ -0,0 +1,101 @@
  1 +require 'test/unit'
  2 +require File.expand_path(File.join(File.dirname(__FILE__), '../lib/geometry')) unless Object.const_defined?(:Geometry)
  3 +
  4 +class GeometryTest < Test::Unit::TestCase
  5 + def test_should_resize
  6 + assert_geometry 50, 64,
  7 + "50x50" => [39, 50],
  8 + "60x60" => [47, 60],
  9 + "100x100" => [78, 100]
  10 + end
  11 +
  12 + def test_should_resize_no_width
  13 + assert_geometry 50, 64,
  14 + "x50" => [39, 50],
  15 + "x60" => [47, 60],
  16 + "x100" => [78, 100]
  17 + end
  18 +
  19 + def test_should_resize_no_height
  20 + assert_geometry 50, 64,
  21 + "50" => [50, 64],
  22 + "60" => [60, 77],
  23 + "100" => [100, 128]
  24 + end
  25 +
  26 + def test_should_resize_with_percent
  27 + assert_geometry 50, 64,
  28 + "50x50%" => [25, 32],
  29 + "60x60%" => [30, 38],
  30 + "120x112%" => [60, 72]
  31 + end
  32 +
  33 + def test_should_resize_with_percent_and_no_width
  34 + assert_geometry 50, 64,
  35 + "x50%" => [50, 32],
  36 + "x60%" => [50, 38],
  37 + "x112%" => [50, 72]
  38 + end
  39 +
  40 + def test_should_resize_with_percent_and_no_height
  41 + assert_geometry 50, 64,
  42 + "50%" => [25, 32],
  43 + "60%" => [30, 38],
  44 + "120%" => [60, 77]
  45 + end
  46 +
  47 + def test_should_resize_with_less
  48 + assert_geometry 50, 64,
  49 + "50x50<" => [50, 64],
  50 + "60x60<" => [50, 64],
  51 + "100x100<" => [78, 100],
  52 + "100x112<" => [88, 112],
  53 + "40x70<" => [50, 64]
  54 + end
  55 +
  56 + def test_should_resize_with_less_and_no_width
  57 + assert_geometry 50, 64,
  58 + "x50<" => [50, 64],
  59 + "x60<" => [50, 64],
  60 + "x100<" => [78, 100]
  61 + end
  62 +
  63 + def test_should_resize_with_less_and_no_height
  64 + assert_geometry 50, 64,
  65 + "50<" => [50, 64],
  66 + "60<" => [60, 77],
  67 + "100<" => [100, 128]
  68 + end
  69 +
  70 + def test_should_resize_with_greater
  71 + assert_geometry 50, 64,
  72 + "50x50>" => [39, 50],
  73 + "60x60>" => [47, 60],
  74 + "100x100>" => [50, 64],
  75 + "100x112>" => [50, 64],
  76 + "40x70>" => [40, 51]
  77 + end
  78 +
  79 + def test_should_resize_with_greater_and_no_width
  80 + assert_geometry 50, 64,
  81 + "x40>" => [31, 40],
  82 + "x60>" => [47, 60],
  83 + "x100>" => [50, 64]
  84 + end
  85 +
  86 + def test_should_resize_with_greater_and_no_height
  87 + assert_geometry 50, 64,
  88 + "40>" => [40, 51],
  89 + "60>" => [50, 64],
  90 + "100>" => [50, 64]
  91 + end
  92 +
  93 + protected
  94 + def assert_geometry(width, height, values)
  95 + values.each do |geo, result|
  96 + # run twice to verify the Geometry string isn't modified after a run
  97 + geo = Geometry.from_s(geo)
  98 + 2.times { assert_equal result, [width, height] / geo }
  99 + end
  100 + end
  101 +end
0 \ No newline at end of file 102 \ No newline at end of file
vendor/plugins/attachment_fu/test/processors/image_science_test.rb 0 → 100644
@@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
  1 +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper'))
  2 +
  3 +class ImageScienceTest < Test::Unit::TestCase
  4 + attachment_model ImageScienceAttachment
  5 +
  6 + if Object.const_defined?(:ImageScience)
  7 + def test_should_resize_image
  8 + attachment = upload_file :filename => '/files/rails.png'
  9 + assert_valid attachment
  10 + assert attachment.image?
  11 + # test image science thumbnail
  12 + assert_equal 42, attachment.width
  13 + assert_equal 55, attachment.height
  14 +
  15 + thumb = attachment.thumbnails.detect { |t| t.filename =~ /_thumb/ }
  16 + geo = attachment.thumbnails.detect { |t| t.filename =~ /_geometry/ }
  17 +
  18 + # test exact resize dimensions
  19 + assert_equal 50, thumb.width
  20 + assert_equal 51, thumb.height
  21 +
  22 + # test geometry string
  23 + assert_equal 31, geo.width
  24 + assert_equal 41, geo.height
  25 + end
  26 + else
  27 + def test_flunk
  28 + puts "ImageScience not loaded, tests not running"
  29 + end
  30 + end
  31 +end
0 \ No newline at end of file 32 \ No newline at end of file
vendor/plugins/attachment_fu/test/processors/mini_magick_test.rb 0 → 100644
@@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
  1 +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper'))
  2 +
  3 +class MiniMagickTest < Test::Unit::TestCase
  4 + attachment_model MiniMagickAttachment
  5 +
  6 + if Object.const_defined?(:MiniMagick)
  7 + def test_should_resize_image
  8 + attachment = upload_file :filename => '/files/rails.png'
  9 + assert_valid attachment
  10 + assert attachment.image?
  11 + # test MiniMagick thumbnail
  12 + assert_equal 43, attachment.width
  13 + assert_equal 55, attachment.height
  14 +
  15 + thumb = attachment.thumbnails.detect { |t| t.filename =~ /_thumb/ }
  16 + geo = attachment.thumbnails.detect { |t| t.filename =~ /_geometry/ }
  17 +
  18 + # test exact resize dimensions
  19 + assert_equal 50, thumb.width
  20 + assert_equal 51, thumb.height
  21 +
  22 + # test geometry string
  23 + assert_equal 31, geo.width
  24 + assert_equal 40, geo.height
  25 + end
  26 + else
  27 + def test_flunk
  28 + puts "MiniMagick not loaded, tests not running"
  29 + end
  30 + end
  31 +end
vendor/plugins/attachment_fu/test/processors/rmagick_test.rb 0 → 100644
@@ -0,0 +1,241 @@ @@ -0,0 +1,241 @@
  1 +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper'))
  2 +
  3 +class RmagickTest < Test::Unit::TestCase
  4 + attachment_model Attachment
  5 +
  6 + if Object.const_defined?(:Magick)
  7 + def test_should_create_image_from_uploaded_file
  8 + assert_created do
  9 + attachment = upload_file :filename => '/files/rails.png'
  10 + assert_valid attachment
  11 + assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file)
  12 + assert attachment.image?
  13 + assert !attachment.size.zero?
  14 + #assert_equal 1784, attachment.size
  15 + assert_equal 50, attachment.width
  16 + assert_equal 64, attachment.height
  17 + assert_equal '50x64', attachment.image_size
  18 + end
  19 + end
  20 +
  21 + def test_should_create_image_from_uploaded_file_with_custom_content_type
  22 + assert_created do
  23 + attachment = upload_file :content_type => 'foo/bar', :filename => '/files/rails.png'
  24 + assert_valid attachment
  25 + assert !attachment.image?
  26 + assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file)
  27 + assert !attachment.size.zero?
  28 + #assert_equal 1784, attachment.size
  29 + assert_nil attachment.width
  30 + assert_nil attachment.height
  31 + assert_equal [], attachment.thumbnails
  32 + end
  33 + end
  34 +
  35 + def test_should_create_thumbnail
  36 + attachment = upload_file :filename => '/files/rails.png'
  37 +
  38 + assert_created do
  39 + basename, ext = attachment.filename.split '.'
  40 + thumbnail = attachment.create_or_update_thumbnail(attachment.create_temp_file, 'thumb', 50, 50)
  41 + assert_valid thumbnail
  42 + assert !thumbnail.size.zero?
  43 + #assert_in_delta 4673, thumbnail.size, 2
  44 + assert_equal 50, thumbnail.width
  45 + assert_equal 50, thumbnail.height
  46 + assert_equal [thumbnail.id], attachment.thumbnails.collect(&:id)
  47 + assert_equal attachment.id, thumbnail.parent_id if thumbnail.respond_to?(:parent_id)
  48 + assert_equal "#{basename}_thumb.#{ext}", thumbnail.filename
  49 + end
  50 + end
  51 +
  52 + def test_should_create_thumbnail_with_geometry_string
  53 + attachment = upload_file :filename => '/files/rails.png'
  54 +
  55 + assert_created do
  56 + basename, ext = attachment.filename.split '.'
  57 + thumbnail = attachment.create_or_update_thumbnail(attachment.create_temp_file, 'thumb', 'x50')
  58 + assert_valid thumbnail
  59 + assert !thumbnail.size.zero?
  60 + #assert_equal 3915, thumbnail.size
  61 + assert_equal 39, thumbnail.width
  62 + assert_equal 50, thumbnail.height
  63 + assert_equal [thumbnail], attachment.thumbnails
  64 + assert_equal attachment.id, thumbnail.parent_id if thumbnail.respond_to?(:parent_id)
  65 + assert_equal "#{basename}_thumb.#{ext}", thumbnail.filename
  66 + end
  67 + end
  68 +
  69 + def test_should_resize_image(klass = ImageAttachment)
  70 + attachment_model klass
  71 + assert_equal [50, 50], attachment_model.attachment_options[:resize_to]
  72 + attachment = upload_file :filename => '/files/rails.png'
  73 + assert_valid attachment
  74 + assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file)
  75 + assert attachment.image?
  76 + assert !attachment.size.zero?
  77 + #assert_in_delta 4673, attachment.size, 2
  78 + assert_equal 50, attachment.width
  79 + assert_equal 50, attachment.height
  80 + end
  81 +
  82 + test_against_subclass :test_should_resize_image, ImageAttachment
  83 +
  84 + def test_should_resize_image_with_geometry(klass = ImageOrPdfAttachment)
  85 + attachment_model klass
  86 + assert_equal 'x50', attachment_model.attachment_options[:resize_to]
  87 + attachment = upload_file :filename => '/files/rails.png'
  88 + assert_valid attachment
  89 + assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file)
  90 + assert attachment.image?
  91 + assert !attachment.size.zero?
  92 + #assert_equal 3915, attachment.size
  93 + assert_equal 39, attachment.width
  94 + assert_equal 50, attachment.height
  95 + end
  96 +
  97 + test_against_subclass :test_should_resize_image_with_geometry, ImageOrPdfAttachment
  98 +
  99 + def test_should_give_correct_thumbnail_filenames(klass = ImageWithThumbsFileAttachment)
  100 + attachment_model klass
  101 + assert_created 3 do
  102 + attachment = upload_file :filename => '/files/rails.png'
  103 + thumb = attachment.thumbnails.detect { |t| t.filename =~ /_thumb/ }
  104 + geo = attachment.thumbnails.detect { |t| t.filename =~ /_geometry/ }
  105 +
  106 + [attachment, thumb, geo].each { |record| assert_valid record }
  107 +
  108 + assert_match /rails\.png$/, attachment.full_filename
  109 + assert_match /rails_geometry\.png$/, attachment.full_filename(:geometry)
  110 + assert_match /rails_thumb\.png$/, attachment.full_filename(:thumb)
  111 + end
  112 + end
  113 +
  114 + test_against_subclass :test_should_give_correct_thumbnail_filenames, ImageWithThumbsFileAttachment
  115 +
  116 + def test_should_automatically_create_thumbnails(klass = ImageWithThumbsAttachment)
  117 + attachment_model klass
  118 + assert_created 3 do
  119 + attachment = upload_file :filename => '/files/rails.png'
  120 + assert_valid attachment
  121 + assert !attachment.size.zero?
  122 + #assert_equal 1784, attachment.size
  123 + assert_equal 55, attachment.width
  124 + assert_equal 55, attachment.height
  125 + assert_equal 2, attachment.thumbnails.length
  126 + assert_equal 1.0, attachment.aspect_ratio
  127 +
  128 + thumb = attachment.thumbnails.detect { |t| t.filename =~ /_thumb/ }
  129 + assert !thumb.new_record?, thumb.errors.full_messages.join("\n")
  130 + assert !thumb.size.zero?
  131 + #assert_in_delta 4673, thumb.size, 2
  132 + assert_equal 50, thumb.width
  133 + assert_equal 50, thumb.height
  134 + assert_equal 1.0, thumb.aspect_ratio
  135 +
  136 + geo = attachment.thumbnails.detect { |t| t.filename =~ /_geometry/ }
  137 + assert !geo.new_record?, geo.errors.full_messages.join("\n")
  138 + assert !geo.size.zero?
  139 + #assert_equal 3915, geo.size
  140 + assert_equal 50, geo.width
  141 + assert_equal 50, geo.height
  142 + assert_equal 1.0, geo.aspect_ratio
  143 + end
  144 + end
  145 +
  146 + test_against_subclass :test_should_automatically_create_thumbnails, ImageWithThumbsAttachment
  147 +
  148 + # same as above method, but test it on a file model
  149 + test_against_class :test_should_automatically_create_thumbnails, ImageWithThumbsFileAttachment
  150 + test_against_subclass :test_should_automatically_create_thumbnails_on_class, ImageWithThumbsFileAttachment
  151 +
  152 + def test_should_use_thumbnail_subclass(klass = ImageWithThumbsClassFileAttachment)
  153 + attachment_model klass
  154 + attachment = nil
  155 + assert_difference ImageThumbnail, :count do
  156 + attachment = upload_file :filename => '/files/rails.png'
  157 + assert_valid attachment
  158 + end
  159 + assert_kind_of ImageThumbnail, attachment.thumbnails.first
  160 + assert_equal attachment.id, attachment.thumbnails.first.parent.id
  161 + assert_kind_of FileAttachment, attachment.thumbnails.first.parent
  162 + assert_equal 'rails_thumb.png', attachment.thumbnails.first.filename
  163 + assert_equal attachment.thumbnails.first.full_filename, attachment.full_filename(attachment.thumbnails.first.thumbnail),
  164 + "#full_filename does not use thumbnail class' path."
  165 + assert_equal attachment.destroy attachment
  166 + end
  167 +
  168 + test_against_subclass :test_should_use_thumbnail_subclass, ImageWithThumbsClassFileAttachment
  169 +
  170 + def test_should_remove_old_thumbnail_files_when_updating(klass = ImageWithThumbsFileAttachment)
  171 + attachment_model klass
  172 + attachment = nil
  173 + assert_created 3 do
  174 + attachment = upload_file :filename => '/files/rails.png'
  175 + end
  176 +
  177 + old_filenames = [attachment.full_filename] + attachment.thumbnails.collect(&:full_filename)
  178 +
  179 + assert_not_created do
  180 + use_temp_file "files/rails.png" do |file|
  181 + attachment.filename = 'rails2.png'
  182 + attachment.temp_path = File.join(fixture_path, file)
  183 + attachment.save
  184 + new_filenames = [attachment.reload.full_filename] + attachment.thumbnails.collect { |t| t.reload.full_filename }
  185 + new_filenames.each { |f| assert File.exists?(f), "#{f} does not exist" }
  186 + old_filenames.each { |f| assert !File.exists?(f), "#{f} still exists" }
  187 + end
  188 + end
  189 + end
  190 +
  191 + test_against_subclass :test_should_remove_old_thumbnail_files_when_updating, ImageWithThumbsFileAttachment
  192 +
  193 + def test_should_delete_file_when_in_file_system_when_attachment_record_destroyed(klass = ImageWithThumbsFileAttachment)
  194 + attachment_model klass
  195 + attachment = upload_file :filename => '/files/rails.png'
  196 + filenames = [attachment.full_filename] + attachment.thumbnails.collect(&:full_filename)
  197 + filenames.each { |f| assert File.exists?(f), "#{f} never existed to delete on destroy" }
  198 + attachment.destroy
  199 + filenames.each { |f| assert !File.exists?(f), "#{f} still exists" }
  200 + end
  201 +
  202 + test_against_subclass :test_should_delete_file_when_in_file_system_when_attachment_record_destroyed, ImageWithThumbsFileAttachment
  203 +
  204 + def test_should_overwrite_old_thumbnail_records_when_updating(klass = ImageWithThumbsAttachment)
  205 + attachment_model klass
  206 + attachment = nil
  207 + assert_created 3 do
  208 + attachment = upload_file :filename => '/files/rails.png'
  209 + end
  210 + assert_not_created do # no new db_file records
  211 + use_temp_file "files/rails.png" do |file|
  212 + attachment.filename = 'rails2.png'
  213 + attachment.temp_path = File.join(fixture_path, file)
  214 + attachment.save!
  215 + end
  216 + end
  217 + end
  218 +
  219 + test_against_subclass :test_should_overwrite_old_thumbnail_records_when_updating, ImageWithThumbsAttachment
  220 +
  221 + def test_should_overwrite_old_thumbnail_records_when_renaming(klass = ImageWithThumbsAttachment)
  222 + attachment_model klass
  223 + attachment = nil
  224 + assert_created 3 do
  225 + attachment = upload_file :class => klass, :filename => '/files/rails.png'
  226 + end
  227 + assert_not_created do # no new db_file records
  228 + attachment.filename = 'rails2.png'
  229 + attachment.save
  230 + assert !attachment.reload.size.zero?
  231 + assert_equal 'rails2.png', attachment.filename
  232 + end
  233 + end
  234 +
  235 + test_against_subclass :test_should_overwrite_old_thumbnail_records_when_renaming, ImageWithThumbsAttachment
  236 + else
  237 + def test_flunk
  238 + puts "RMagick not installed, no tests running"
  239 + end
  240 + end
  241 +end
0 \ No newline at end of file 242 \ No newline at end of file
vendor/plugins/attachment_fu/test/schema.rb 0 → 100644
@@ -0,0 +1,86 @@ @@ -0,0 +1,86 @@
  1 +ActiveRecord::Schema.define(:version => 0) do
  2 + create_table :attachments, :force => true do |t|
  3 + t.column :db_file_id, :integer
  4 + t.column :parent_id, :integer
  5 + t.column :thumbnail, :string
  6 + t.column :filename, :string, :limit => 255
  7 + t.column :content_type, :string, :limit => 255
  8 + t.column :size, :integer
  9 + t.column :width, :integer
  10 + t.column :height, :integer
  11 + t.column :aspect_ratio, :float
  12 + end
  13 +
  14 + create_table :file_attachments, :force => true do |t|
  15 + t.column :parent_id, :integer
  16 + t.column :thumbnail, :string
  17 + t.column :filename, :string, :limit => 255
  18 + t.column :content_type, :string, :limit => 255
  19 + t.column :size, :integer
  20 + t.column :width, :integer
  21 + t.column :height, :integer
  22 + t.column :type, :string
  23 + t.column :aspect_ratio, :float
  24 + end
  25 +
  26 + create_table :image_science_attachments, :force => true do |t|
  27 + t.column :parent_id, :integer
  28 + t.column :thumbnail, :string
  29 + t.column :filename, :string, :limit => 255
  30 + t.column :content_type, :string, :limit => 255
  31 + t.column :size, :integer
  32 + t.column :width, :integer
  33 + t.column :height, :integer
  34 + t.column :type, :string
  35 + end
  36 +
  37 + create_table :mini_magick_attachments, :force => true do |t|
  38 + t.column :parent_id, :integer
  39 + t.column :thumbnail, :string
  40 + t.column :filename, :string, :limit => 255
  41 + t.column :content_type, :string, :limit => 255
  42 + t.column :size, :integer
  43 + t.column :width, :integer
  44 + t.column :height, :integer
  45 + t.column :type, :string
  46 + end
  47 +
  48 + create_table :mini_magick_attachments, :force => true do |t|
  49 + t.column :parent_id, :integer
  50 + t.column :thumbnail, :string
  51 + t.column :filename, :string, :limit => 255
  52 + t.column :content_type, :string, :limit => 255
  53 + t.column :size, :integer
  54 + t.column :width, :integer
  55 + t.column :height, :integer
  56 + t.column :type, :string
  57 + end
  58 +
  59 + create_table :orphan_attachments, :force => true do |t|
  60 + t.column :db_file_id, :integer
  61 + t.column :filename, :string, :limit => 255
  62 + t.column :content_type, :string, :limit => 255
  63 + t.column :size, :integer
  64 + end
  65 +
  66 + create_table :minimal_attachments, :force => true do |t|
  67 + t.column :size, :integer
  68 + t.column :content_type, :string, :limit => 255
  69 + end
  70 +
  71 + create_table :db_files, :force => true do |t|
  72 + t.column :data, :binary
  73 + end
  74 +
  75 + create_table :s3_attachments, :force => true do |t|
  76 + t.column :parent_id, :integer
  77 + t.column :thumbnail, :string
  78 + t.column :filename, :string, :limit => 255
  79 + t.column :content_type, :string, :limit => 255
  80 + t.column :size, :integer
  81 + t.column :width, :integer
  82 + t.column :height, :integer
  83 + t.column :type, :string
  84 + t.column :aspect_ratio, :float
  85 + end
  86 +end
0 \ No newline at end of file 87 \ No newline at end of file
vendor/plugins/attachment_fu/test/test_helper.rb 0 → 100644
@@ -0,0 +1,142 @@ @@ -0,0 +1,142 @@
  1 +$:.unshift(File.dirname(__FILE__) + '/../lib')
  2 +
  3 +ENV['RAILS_ENV'] = 'test'
  4 +
  5 +require 'test/unit'
  6 +require File.expand_path(File.join(File.dirname(__FILE__), '../../../../config/environment.rb'))
  7 +require 'breakpoint'
  8 +require 'active_record/fixtures'
  9 +require 'action_controller/test_process'
  10 +
  11 +config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
  12 +ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
  13 +
  14 +db_adapter = ENV['DB']
  15 +
  16 +# no db passed, try one of these fine config-free DBs before bombing.
  17 +db_adapter ||=
  18 + begin
  19 + require 'rubygems'
  20 + require 'sqlite'
  21 + 'sqlite'
  22 + rescue MissingSourceFile
  23 + begin
  24 + require 'sqlite3'
  25 + 'sqlite3'
  26 + rescue MissingSourceFile
  27 + end
  28 + end
  29 +
  30 +if db_adapter.nil?
  31 + raise "No DB Adapter selected. Pass the DB= option to pick one, or install Sqlite or Sqlite3."
  32 +end
  33 +
  34 +ActiveRecord::Base.establish_connection(config[db_adapter])
  35 +
  36 +load(File.dirname(__FILE__) + "/schema.rb")
  37 +
  38 +Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures"
  39 +$LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path)
  40 +
  41 +class Test::Unit::TestCase #:nodoc:
  42 + include ActionController::TestProcess
  43 + def create_fixtures(*table_names)
  44 + if block_given?
  45 + Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield }
  46 + else
  47 + Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names)
  48 + end
  49 + end
  50 +
  51 + def setup
  52 + Attachment.saves = 0
  53 + DbFile.transaction { [Attachment, FileAttachment, OrphanAttachment, MinimalAttachment, DbFile].each { |klass| klass.delete_all } }
  54 + attachment_model self.class.attachment_model
  55 + end
  56 +
  57 + def teardown
  58 + FileUtils.rm_rf File.join(File.dirname(__FILE__), 'files')
  59 + end
  60 +
  61 + self.use_transactional_fixtures = true
  62 + self.use_instantiated_fixtures = false
  63 +
  64 + def self.attachment_model(klass = nil)
  65 + @attachment_model = klass if klass
  66 + @attachment_model
  67 + end
  68 +
  69 + def self.test_against_class(test_method, klass, subclass = false)
  70 + define_method("#{test_method}_on_#{:sub if subclass}class") do
  71 + klass = Class.new(klass) if subclass
  72 + attachment_model klass
  73 + send test_method, klass
  74 + end
  75 + end
  76 +
  77 + def self.test_against_subclass(test_method, klass)
  78 + test_against_class test_method, klass, true
  79 + end
  80 +
  81 + protected
  82 + def upload_file(options = {})
  83 + use_temp_file options[:filename] do |file|
  84 + att = attachment_model.create :uploaded_data => fixture_file_upload(file, options[:content_type] || 'image/png')
  85 + att.reload unless att.new_record?
  86 + return att
  87 + end
  88 + end
  89 +
  90 + def use_temp_file(fixture_filename)
  91 + temp_path = File.join('/tmp', File.basename(fixture_filename))
  92 + FileUtils.mkdir_p File.join(fixture_path, 'tmp')
  93 + FileUtils.cp File.join(fixture_path, fixture_filename), File.join(fixture_path, temp_path)
  94 + yield temp_path
  95 + ensure
  96 + FileUtils.rm_rf File.join(fixture_path, 'tmp')
  97 + end
  98 +
  99 + def assert_created(num = 1)
  100 + assert_difference attachment_model.base_class, :count, num do
  101 + if attachment_model.included_modules.include? DbFile
  102 + assert_difference DbFile, :count, num do
  103 + yield
  104 + end
  105 + else
  106 + yield
  107 + end
  108 + end
  109 + end
  110 +
  111 + def assert_not_created
  112 + assert_created(0) { yield }
  113 + end
  114 +
  115 + def should_reject_by_size_with(klass)
  116 + attachment_model klass
  117 + assert_not_created do
  118 + attachment = upload_file :filename => '/files/rails.png'
  119 + assert attachment.new_record?
  120 + assert attachment.errors.on(:size)
  121 + assert_nil attachment.db_file if attachment.respond_to?(:db_file)
  122 + end
  123 + end
  124 +
  125 + def assert_difference(object, method = nil, difference = 1)
  126 + initial_value = object.send(method)
  127 + yield
  128 + assert_equal initial_value + difference, object.send(method)
  129 + end
  130 +
  131 + def assert_no_difference(object, method, &block)
  132 + assert_difference object, method, 0, &block
  133 + end
  134 +
  135 + def attachment_model(klass = nil)
  136 + @attachment_model = klass if klass
  137 + @attachment_model
  138 + end
  139 +end
  140 +
  141 +require File.join(File.dirname(__FILE__), 'fixtures/attachment')
  142 +require File.join(File.dirname(__FILE__), 'base_attachment_tests')
0 \ No newline at end of file 143 \ No newline at end of file
vendor/plugins/attachment_fu/test/validation_test.rb 0 → 100644
@@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
  1 +require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
  2 +
  3 +class ValidationTest < Test::Unit::TestCase
  4 + def test_should_invalidate_big_files
  5 + @attachment = SmallAttachment.new
  6 + assert !@attachment.valid?
  7 + assert @attachment.errors.on(:size)
  8 +
  9 + @attachment.size = 2000
  10 + assert !@attachment.valid?
  11 + assert @attachment.errors.on(:size), @attachment.errors.full_messages.to_sentence
  12 +
  13 + @attachment.size = 1000
  14 + assert !@attachment.valid?
  15 + assert_nil @attachment.errors.on(:size)
  16 + end
  17 +
  18 + def test_should_invalidate_small_files
  19 + @attachment = BigAttachment.new
  20 + assert !@attachment.valid?
  21 + assert @attachment.errors.on(:size)
  22 +
  23 + @attachment.size = 2000
  24 + assert !@attachment.valid?
  25 + assert @attachment.errors.on(:size), @attachment.errors.full_messages.to_sentence
  26 +
  27 + @attachment.size = 1.megabyte
  28 + assert !@attachment.valid?
  29 + assert_nil @attachment.errors.on(:size)
  30 + end
  31 +
  32 + def test_should_validate_content_type
  33 + @attachment = PdfAttachment.new
  34 + assert !@attachment.valid?
  35 + assert @attachment.errors.on(:content_type)
  36 +
  37 + @attachment.content_type = 'foo'
  38 + assert !@attachment.valid?
  39 + assert @attachment.errors.on(:content_type)
  40 +
  41 + @attachment.content_type = 'pdf'
  42 + assert !@attachment.valid?
  43 + assert_nil @attachment.errors.on(:content_type)
  44 + end
  45 +
  46 + def test_should_require_filename
  47 + @attachment = Attachment.new
  48 + assert !@attachment.valid?
  49 + assert @attachment.errors.on(:filename)
  50 +
  51 + @attachment.filename = 'foo'
  52 + assert !@attachment.valid?
  53 + assert_nil @attachment.errors.on(:filename)
  54 + end
  55 +end
0 \ No newline at end of file 56 \ No newline at end of file
vendor/plugins/doc_browser/COPYING 0 → 100644
@@ -0,0 +1,674 @@ @@ -0,0 +1,674 @@
  1 + GNU GENERAL PUBLIC LICENSE
  2 + Version 3, 29 June 2007
  3 +
  4 + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
  5 + Everyone is permitted to copy and distribute verbatim copies
  6 + of this license document, but changing it is not allowed.
  7 +
  8 + Preamble
  9 +
  10 + The GNU General Public License is a free, copyleft license for
  11 +software and other kinds of works.
  12 +
  13 + The licenses for most software and other practical works are designed
  14 +to take away your freedom to share and change the works. By contrast,
  15 +the GNU General Public License is intended to guarantee your freedom to
  16 +share and change all versions of a program--to make sure it remains free
  17 +software for all its users. We, the Free Software Foundation, use the
  18 +GNU General Public License for most of our software; it applies also to
  19 +any other work released this way by its authors. You can apply it to
  20 +your programs, too.
  21 +
  22 + When we speak of free software, we are referring to freedom, not
  23 +price. Our General Public Licenses are designed to make sure that you
  24 +have the freedom to distribute copies of free software (and charge for
  25 +them if you wish), that you receive source code or can get it if you
  26 +want it, that you can change the software or use pieces of it in new
  27 +free programs, and that you know you can do these things.
  28 +
  29 + To protect your rights, we need to prevent others from denying you
  30 +these rights or asking you to surrender the rights. Therefore, you have
  31 +certain responsibilities if you distribute copies of the software, or if
  32 +you modify it: responsibilities to respect the freedom of others.
  33 +
  34 + For example, if you distribute copies of such a program, whether
  35 +gratis or for a fee, you must pass on to the recipients the same
  36 +freedoms that you received. You must make sure that they, too, receive
  37 +or can get the source code. And you must show them these terms so they
  38 +know their rights.
  39 +
  40 + Developers that use the GNU GPL protect your rights with two steps:
  41 +(1) assert copyright on the software, and (2) offer you this License
  42 +giving you legal permission to copy, distribute and/or modify it.
  43 +
  44 + For the developers' and authors' protection, the GPL clearly explains
  45 +that there is no warranty for this free software. For both users' and
  46 +authors' sake, the GPL requires that modified versions be marked as
  47 +changed, so that their problems will not be attributed erroneously to
  48 +authors of previous versions.
  49 +
  50 + Some devices are designed to deny users access to install or run
  51 +modified versions of the software inside them, although the manufacturer
  52 +can do so. This is fundamentally incompatible with the aim of
  53 +protecting users' freedom to change the software. The systematic
  54 +pattern of such abuse occurs in the area of products for individuals to
  55 +use, which is precisely where it is most unacceptable. Therefore, we
  56 +have designed this version of the GPL to prohibit the practice for those
  57 +products. If such problems arise substantially in other domains, we
  58 +stand ready to extend this provision to those domains in future versions
  59 +of the GPL, as needed to protect the freedom of users.
  60 +
  61 + Finally, every program is threatened constantly by software patents.
  62 +States should not allow patents to restrict development and use of
  63 +software on general-purpose computers, but in those that do, we wish to
  64 +avoid the special danger that patents applied to a free program could
  65 +make it effectively proprietary. To prevent this, the GPL assures that
  66 +patents cannot be used to render the program non-free.
  67 +
  68 + The precise terms and conditions for copying, distribution and
  69 +modification follow.
  70 +
  71 + TERMS AND CONDITIONS
  72 +
  73 + 0. Definitions.
  74 +
  75 + "This License" refers to version 3 of the GNU General Public License.
  76 +
  77 + "Copyright" also means copyright-like laws that apply to other kinds of
  78 +works, such as semiconductor masks.
  79 +
  80 + "The Program" refers to any copyrightable work licensed under this
  81 +License. Each licensee is addressed as "you". "Licensees" and
  82 +"recipients" may be individuals or organizations.
  83 +
  84 + To "modify" a work means to copy from or adapt all or part of the work
  85 +in a fashion requiring copyright permission, other than the making of an
  86 +exact copy. The resulting work is called a "modified version" of the
  87 +earlier work or a work "based on" the earlier work.
  88 +
  89 + A "covered work" means either the unmodified Program or a work based
  90 +on the Program.
  91 +
  92 + To "propagate" a work means to do anything with it that, without
  93 +permission, would make you directly or secondarily liable for
  94 +infringement under applicable copyright law, except executing it on a
  95 +computer or modifying a private copy. Propagation includes copying,
  96 +distribution (with or without modification), making available to the
  97 +public, and in some countries other activities as well.
  98 +
  99 + To "convey" a work means any kind of propagation that enables other
  100 +parties to make or receive copies. Mere interaction with a user through
  101 +a computer network, with no transfer of a copy, is not conveying.
  102 +
  103 + An interactive user interface displays "Appropriate Legal Notices"
  104 +to the extent that it includes a convenient and prominently visible
  105 +feature that (1) displays an appropriate copyright notice, and (2)
  106 +tells the user that there is no warranty for the work (except to the
  107 +extent that warranties are provided), that licensees may convey the
  108 +work under this License, and how to view a copy of this License. If
  109 +the interface presents a list of user commands or options, such as a
  110 +menu, a prominent item in the list meets this criterion.
  111 +
  112 + 1. Source Code.
  113 +
  114 + The "source code" for a work means the preferred form of the work
  115 +for making modifications to it. "Object code" means any non-source
  116 +form of a work.
  117 +
  118 + A "Standard Interface" means an interface that either is an official
  119 +standard defined by a recognized standards body, or, in the case of
  120 +interfaces specified for a particular programming language, one that
  121 +is widely used among developers working in that language.
  122 +
  123 + The "System Libraries" of an executable work include anything, other
  124 +than the work as a whole, that (a) is included in the normal form of
  125 +packaging a Major Component, but which is not part of that Major
  126 +Component, and (b) serves only to enable use of the work with that
  127 +Major Component, or to implement a Standard Interface for which an
  128 +implementation is available to the public in source code form. A
  129 +"Major Component", in this context, means a major essential component
  130 +(kernel, window system, and so on) of the specific operating system
  131 +(if any) on which the executable work runs, or a compiler used to
  132 +produce the work, or an object code interpreter used to run it.
  133 +
  134 + The "Corresponding Source" for a work in object code form means all
  135 +the source code needed to generate, install, and (for an executable
  136 +work) run the object code and to modify the work, including scripts to
  137 +control those activities. However, it does not include the work's
  138 +System Libraries, or general-purpose tools or generally available free
  139 +programs which are used unmodified in performing those activities but
  140 +which are not part of the work. For example, Corresponding Source
  141 +includes interface definition files associated with source files for
  142 +the work, and the source code for shared libraries and dynamically
  143 +linked subprograms that the work is specifically designed to require,
  144 +such as by intimate data communication or control flow between those
  145 +subprograms and other parts of the work.
  146 +
  147 + The Corresponding Source need not include anything that users
  148 +can regenerate automatically from other parts of the Corresponding
  149 +Source.
  150 +
  151 + The Corresponding Source for a work in source code form is that
  152 +same work.
  153 +
  154 + 2. Basic Permissions.
  155 +
  156 + All rights granted under this License are granted for the term of
  157 +copyright on the Program, and are irrevocable provided the stated
  158 +conditions are met. This License explicitly affirms your unlimited
  159 +permission to run the unmodified Program. The output from running a
  160 +covered work is covered by this License only if the output, given its
  161 +content, constitutes a covered work. This License acknowledges your
  162 +rights of fair use or other equivalent, as provided by copyright law.
  163 +
  164 + You may make, run and propagate covered works that you do not
  165 +convey, without conditions so long as your license otherwise remains
  166 +in force. You may convey covered works to others for the sole purpose
  167 +of having them make modifications exclusively for you, or provide you
  168 +with facilities for running those works, provided that you comply with
  169 +the terms of this License in conveying all material for which you do
  170 +not control copyright. Those thus making or running the covered works
  171 +for you must do so exclusively on your behalf, under your direction
  172 +and control, on terms that prohibit them from making any copies of
  173 +your copyrighted material outside their relationship with you.
  174 +
  175 + Conveying under any other circumstances is permitted solely under
  176 +the conditions stated below. Sublicensing is not allowed; section 10
  177 +makes it unnecessary.
  178 +
  179 + 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
  180 +
  181 + No covered work shall be deemed part of an effective technological
  182 +measure under any applicable law fulfilling obligations under article
  183 +11 of the WIPO copyright treaty adopted on 20 December 1996, or
  184 +similar laws prohibiting or restricting circumvention of such
  185 +measures.
  186 +
  187 + When you convey a covered work, you waive any legal power to forbid
  188 +circumvention of technological measures to the extent such circumvention
  189 +is effected by exercising rights under this License with respect to
  190 +the covered work, and you disclaim any intention to limit operation or
  191 +modification of the work as a means of enforcing, against the work's
  192 +users, your or third parties' legal rights to forbid circumvention of
  193 +technological measures.
  194 +
  195 + 4. Conveying Verbatim Copies.
  196 +
  197 + You may convey verbatim copies of the Program's source code as you
  198 +receive it, in any medium, provided that you conspicuously and
  199 +appropriately publish on each copy an appropriate copyright notice;
  200 +keep intact all notices stating that this License and any
  201 +non-permissive terms added in accord with section 7 apply to the code;
  202 +keep intact all notices of the absence of any warranty; and give all
  203 +recipients a copy of this License along with the Program.
  204 +
  205 + You may charge any price or no price for each copy that you convey,
  206 +and you may offer support or warranty protection for a fee.
  207 +
  208 + 5. Conveying Modified Source Versions.
  209 +
  210 + You may convey a work based on the Program, or the modifications to
  211 +produce it from the Program, in the form of source code under the
  212 +terms of section 4, provided that you also meet all of these conditions:
  213 +
  214 + a) The work must carry prominent notices stating that you modified
  215 + it, and giving a relevant date.
  216 +
  217 + b) The work must carry prominent notices stating that it is
  218 + released under this License and any conditions added under section
  219 + 7. This requirement modifies the requirement in section 4 to
  220 + "keep intact all notices".
  221 +
  222 + c) You must license the entire work, as a whole, under this
  223 + License to anyone who comes into possession of a copy. This
  224 + License will therefore apply, along with any applicable section 7
  225 + additional terms, to the whole of the work, and all its parts,
  226 + regardless of how they are packaged. This License gives no
  227 + permission to license the work in any other way, but it does not
  228 + invalidate such permission if you have separately received it.
  229 +
  230 + d) If the work has interactive user interfaces, each must display
  231 + Appropriate Legal Notices; however, if the Program has interactive
  232 + interfaces that do not display Appropriate Legal Notices, your
  233 + work need not make them do so.
  234 +
  235 + A compilation of a covered work with other separate and independent
  236 +works, which are not by their nature extensions of the covered work,
  237 +and which are not combined with it such as to form a larger program,
  238 +in or on a volume of a storage or distribution medium, is called an
  239 +"aggregate" if the compilation and its resulting copyright are not
  240 +used to limit the access or legal rights of the compilation's users
  241 +beyond what the individual works permit. Inclusion of a covered work
  242 +in an aggregate does not cause this License to apply to the other
  243 +parts of the aggregate.
  244 +
  245 + 6. Conveying Non-Source Forms.
  246 +
  247 + You may convey a covered work in object code form under the terms
  248 +of sections 4 and 5, provided that you also convey the
  249 +machine-readable Corresponding Source under the terms of this License,
  250 +in one of these ways:
  251 +
  252 + a) Convey the object code in, or embodied in, a physical product
  253 + (including a physical distribution medium), accompanied by the
  254 + Corresponding Source fixed on a durable physical medium
  255 + customarily used for software interchange.
  256 +
  257 + b) Convey the object code in, or embodied in, a physical product
  258 + (including a physical distribution medium), accompanied by a
  259 + written offer, valid for at least three years and valid for as
  260 + long as you offer spare parts or customer support for that product
  261 + model, to give anyone who possesses the object code either (1) a
  262 + copy of the Corresponding Source for all the software in the
  263 + product that is covered by this License, on a durable physical
  264 + medium customarily used for software interchange, for a price no
  265 + more than your reasonable cost of physically performing this
  266 + conveying of source, or (2) access to copy the
  267 + Corresponding Source from a network server at no charge.
  268 +
  269 + c) Convey individual copies of the object code with a copy of the
  270 + written offer to provide the Corresponding Source. This
  271 + alternative is allowed only occasionally and noncommercially, and
  272 + only if you received the object code with such an offer, in accord
  273 + with subsection 6b.
  274 +
  275 + d) Convey the object code by offering access from a designated
  276 + place (gratis or for a charge), and offer equivalent access to the
  277 + Corresponding Source in the same way through the same place at no
  278 + further charge. You need not require recipients to copy the
  279 + Corresponding Source along with the object code. If the place to
  280 + copy the object code is a network server, the Corresponding Source
  281 + may be on a different server (operated by you or a third party)
  282 + that supports equivalent copying facilities, provided you maintain
  283 + clear directions next to the object code saying where to find the
  284 + Corresponding Source. Regardless of what server hosts the
  285 + Corresponding Source, you remain obligated to ensure that it is
  286 + available for as long as needed to satisfy these requirements.
  287 +
  288 + e) Convey the object code using peer-to-peer transmission, provided
  289 + you inform other peers where the object code and Corresponding
  290 + Source of the work are being offered to the general public at no
  291 + charge under subsection 6d.
  292 +
  293 + A separable portion of the object code, whose source code is excluded
  294 +from the Corresponding Source as a System Library, need not be
  295 +included in conveying the object code work.
  296 +
  297 + A "User Product" is either (1) a "consumer product", which means any
  298 +tangible personal property which is normally used for personal, family,
  299 +or household purposes, or (2) anything designed or sold for incorporation
  300 +into a dwelling. In determining whether a product is a consumer product,
  301 +doubtful cases shall be resolved in favor of coverage. For a particular
  302 +product received by a particular user, "normally used" refers to a
  303 +typical or common use of that class of product, regardless of the status
  304 +of the particular user or of the way in which the particular user
  305 +actually uses, or expects or is expected to use, the product. A product
  306 +is a consumer product regardless of whether the product has substantial
  307 +commercial, industrial or non-consumer uses, unless such uses represent
  308 +the only significant mode of use of the product.
  309 +
  310 + "Installation Information" for a User Product means any methods,
  311 +procedures, authorization keys, or other information required to install
  312 +and execute modified versions of a covered work in that User Product from
  313 +a modified version of its Corresponding Source. The information must
  314 +suffice to ensure that the continued functioning of the modified object
  315 +code is in no case prevented or interfered with solely because
  316 +modification has been made.
  317 +
  318 + If you convey an object code work under this section in, or with, or
  319 +specifically for use in, a User Product, and the conveying occurs as
  320 +part of a transaction in which the right of possession and use of the
  321 +User Product is transferred to the recipient in perpetuity or for a
  322 +fixed term (regardless of how the transaction is characterized), the
  323 +Corresponding Source conveyed under this section must be accompanied
  324 +by the Installation Information. But this requirement does not apply
  325 +if neither you nor any third party retains the ability to install
  326 +modified object code on the User Product (for example, the work has
  327 +been installed in ROM).
  328 +
  329 + The requirement to provide Installation Information does not include a
  330 +requirement to continue to provide support service, warranty, or updates
  331 +for a work that has been modified or installed by the recipient, or for
  332 +the User Product in which it has been modified or installed. Access to a
  333 +network may be denied when the modification itself materially and
  334 +adversely affects the operation of the network or violates the rules and
  335 +protocols for communication across the network.
  336 +
  337 + Corresponding Source conveyed, and Installation Information provided,
  338 +in accord with this section must be in a format that is publicly
  339 +documented (and with an implementation available to the public in
  340 +source code form), and must require no special password or key for
  341 +unpacking, reading or copying.
  342 +
  343 + 7. Additional Terms.
  344 +
  345 + "Additional permissions" are terms that supplement the terms of this
  346 +License by making exceptions from one or more of its conditions.
  347 +Additional permissions that are applicable to the entire Program shall
  348 +be treated as though they were included in this License, to the extent
  349 +that they are valid under applicable law. If additional permissions
  350 +apply only to part of the Program, that part may be used separately
  351 +under those permissions, but the entire Program remains governed by
  352 +this License without regard to the additional permissions.
  353 +
  354 + When you convey a copy of a covered work, you may at your option
  355 +remove any additional permissions from that copy, or from any part of
  356 +it. (Additional permissions may be written to require their own
  357 +removal in certain cases when you modify the work.) You may place
  358 +additional permissions on material, added by you to a covered work,
  359 +for which you have or can give appropriate copyright permission.
  360 +
  361 + Notwithstanding any other provision of this License, for material you
  362 +add to a covered work, you may (if authorized by the copyright holders of
  363 +that material) supplement the terms of this License with terms:
  364 +
  365 + a) Disclaiming warranty or limiting liability differently from the
  366 + terms of sections 15 and 16 of this License; or
  367 +
  368 + b) Requiring preservation of specified reasonable legal notices or
  369 + author attributions in that material or in the Appropriate Legal
  370 + Notices displayed by works containing it; or
  371 +
  372 + c) Prohibiting misrepresentation of the origin of that material, or
  373 + requiring that modified versions of such material be marked in
  374 + reasonable ways as different from the original version; or
  375 +
  376 + d) Limiting the use for publicity purposes of names of licensors or
  377 + authors of the material; or
  378 +
  379 + e) Declining to grant rights under trademark law for use of some
  380 + trade names, trademarks, or service marks; or
  381 +
  382 + f) Requiring indemnification of licensors and authors of that
  383 + material by anyone who conveys the material (or modified versions of
  384 + it) with contractual assumptions of liability to the recipient, for
  385 + any liability that these contractual assumptions directly impose on
  386 + those licensors and authors.
  387 +
  388 + All other non-permissive additional terms are considered "further
  389 +restrictions" within the meaning of section 10. If the Program as you
  390 +received it, or any part of it, contains a notice stating that it is
  391 +governed by this License along with a term that is a further
  392 +restriction, you may remove that term. If a license document contains
  393 +a further restriction but permits relicensing or conveying under this
  394 +License, you may add to a covered work material governed by the terms
  395 +of that license document, provided that the further restriction does
  396 +not survive such relicensing or conveying.
  397 +
  398 + If you add terms to a covered work in accord with this section, you
  399 +must place, in the relevant source files, a statement of the
  400 +additional terms that apply to those files, or a notice indicating
  401 +where to find the applicable terms.
  402 +
  403 + Additional terms, permissive or non-permissive, may be stated in the
  404 +form of a separately written license, or stated as exceptions;
  405 +the above requirements apply either way.
  406 +
  407 + 8. Termination.
  408 +
  409 + You may not propagate or modify a covered work except as expressly
  410 +provided under this License. Any attempt otherwise to propagate or
  411 +modify it is void, and will automatically terminate your rights under
  412 +this License (including any patent licenses granted under the third
  413 +paragraph of section 11).
  414 +
  415 + However, if you cease all violation of this License, then your
  416 +license from a particular copyright holder is reinstated (a)
  417 +provisionally, unless and until the copyright holder explicitly and
  418 +finally terminates your license, and (b) permanently, if the copyright
  419 +holder fails to notify you of the violation by some reasonable means
  420 +prior to 60 days after the cessation.
  421 +
  422 + Moreover, your license from a particular copyright holder is
  423 +reinstated permanently if the copyright holder notifies you of the
  424 +violation by some reasonable means, this is the first time you have
  425 +received notice of violation of this License (for any work) from that
  426 +copyright holder, and you cure the violation prior to 30 days after
  427 +your receipt of the notice.
  428 +
  429 + Termination of your rights under this section does not terminate the
  430 +licenses of parties who have received copies or rights from you under
  431 +this License. If your rights have been terminated and not permanently
  432 +reinstated, you do not qualify to receive new licenses for the same
  433 +material under section 10.
  434 +
  435 + 9. Acceptance Not Required for Having Copies.
  436 +
  437 + You are not required to accept this License in order to receive or
  438 +run a copy of the Program. Ancillary propagation of a covered work
  439 +occurring solely as a consequence of using peer-to-peer transmission
  440 +to receive a copy likewise does not require acceptance. However,
  441 +nothing other than this License grants you permission to propagate or
  442 +modify any covered work. These actions infringe copyright if you do
  443 +not accept this License. Therefore, by modifying or propagating a
  444 +covered work, you indicate your acceptance of this License to do so.
  445 +
  446 + 10. Automatic Licensing of Downstream Recipients.
  447 +
  448 + Each time you convey a covered work, the recipient automatically
  449 +receives a license from the original licensors, to run, modify and
  450 +propagate that work, subject to this License. You are not responsible
  451 +for enforcing compliance by third parties with this License.
  452 +
  453 + An "entity transaction" is a transaction transferring control of an
  454 +organization, or substantially all assets of one, or subdividing an
  455 +organization, or merging organizations. If propagation of a covered
  456 +work results from an entity transaction, each party to that
  457 +transaction who receives a copy of the work also receives whatever
  458 +licenses to the work the party's predecessor in interest had or could
  459 +give under the previous paragraph, plus a right to possession of the
  460 +Corresponding Source of the work from the predecessor in interest, if
  461 +the predecessor has it or can get it with reasonable efforts.
  462 +
  463 + You may not impose any further restrictions on the exercise of the
  464 +rights granted or affirmed under this License. For example, you may
  465 +not impose a license fee, royalty, or other charge for exercise of
  466 +rights granted under this License, and you may not initiate litigation
  467 +(including a cross-claim or counterclaim in a lawsuit) alleging that
  468 +any patent claim is infringed by making, using, selling, offering for
  469 +sale, or importing the Program or any portion of it.
  470 +
  471 + 11. Patents.
  472 +
  473 + A "contributor" is a copyright holder who authorizes use under this
  474 +License of the Program or a work on which the Program is based. The
  475 +work thus licensed is called the contributor's "contributor version".
  476 +
  477 + A contributor's "essential patent claims" are all patent claims
  478 +owned or controlled by the contributor, whether already acquired or
  479 +hereafter acquired, that would be infringed by some manner, permitted
  480 +by this License, of making, using, or selling its contributor version,
  481 +but do not include claims that would be infringed only as a
  482 +consequence of further modification of the contributor version. For
  483 +purposes of this definition, "control" includes the right to grant
  484 +patent sublicenses in a manner consistent with the requirements of
  485 +this License.
  486 +
  487 + Each contributor grants you a non-exclusive, worldwide, royalty-free
  488 +patent license under the contributor's essential patent claims, to
  489 +make, use, sell, offer for sale, import and otherwise run, modify and
  490 +propagate the contents of its contributor version.
  491 +
  492 + In the following three paragraphs, a "patent license" is any express
  493 +agreement or commitment, however denominated, not to enforce a patent
  494 +(such as an express permission to practice a patent or covenant not to
  495 +sue for patent infringement). To "grant" such a patent license to a
  496 +party means to make such an agreement or commitment not to enforce a
  497 +patent against the party.
  498 +
  499 + If you convey a covered work, knowingly relying on a patent license,
  500 +and the Corresponding Source of the work is not available for anyone
  501 +to copy, free of charge and under the terms of this License, through a
  502 +publicly available network server or other readily accessible means,
  503 +then you must either (1) cause the Corresponding Source to be so
  504 +available, or (2) arrange to deprive yourself of the benefit of the
  505 +patent license for this particular work, or (3) arrange, in a manner
  506 +consistent with the requirements of this License, to extend the patent
  507 +license to downstream recipients. "Knowingly relying" means you have
  508 +actual knowledge that, but for the patent license, your conveying the
  509 +covered work in a country, or your recipient's use of the covered work
  510 +in a country, would infringe one or more identifiable patents in that
  511 +country that you have reason to believe are valid.
  512 +
  513 + If, pursuant to or in connection with a single transaction or
  514 +arrangement, you convey, or propagate by procuring conveyance of, a
  515 +covered work, and grant a patent license to some of the parties
  516 +receiving the covered work authorizing them to use, propagate, modify
  517 +or convey a specific copy of the covered work, then the patent license
  518 +you grant is automatically extended to all recipients of the covered
  519 +work and works based on it.
  520 +
  521 + A patent license is "discriminatory" if it does not include within
  522 +the scope of its coverage, prohibits the exercise of, or is
  523 +conditioned on the non-exercise of one or more of the rights that are
  524 +specifically granted under this License. You may not convey a covered
  525 +work if you are a party to an arrangement with a third party that is
  526 +in the business of distributing software, under which you make payment
  527 +to the third party based on the extent of your activity of conveying
  528 +the work, and under which the third party grants, to any of the
  529 +parties who would receive the covered work from you, a discriminatory
  530 +patent license (a) in connection with copies of the covered work
  531 +conveyed by you (or copies made from those copies), or (b) primarily
  532 +for and in connection with specific products or compilations that
  533 +contain the covered work, unless you entered into that arrangement,
  534 +or that patent license was granted, prior to 28 March 2007.
  535 +
  536 + Nothing in this License shall be construed as excluding or limiting
  537 +any implied license or other defenses to infringement that may
  538 +otherwise be available to you under applicable patent law.
  539 +
  540 + 12. No Surrender of Others' Freedom.
  541 +
  542 + If conditions are imposed on you (whether by court order, agreement or
  543 +otherwise) that contradict the conditions of this License, they do not
  544 +excuse you from the conditions of this License. If you cannot convey a
  545 +covered work so as to satisfy simultaneously your obligations under this
  546 +License and any other pertinent obligations, then as a consequence you may
  547 +not convey it at all. For example, if you agree to terms that obligate you
  548 +to collect a royalty for further conveying from those to whom you convey
  549 +the Program, the only way you could satisfy both those terms and this
  550 +License would be to refrain entirely from conveying the Program.
  551 +
  552 + 13. Use with the GNU Affero General Public License.
  553 +
  554 + Notwithstanding any other provision of this License, you have
  555 +permission to link or combine any covered work with a work licensed
  556 +under version 3 of the GNU Affero General Public License into a single
  557 +combined work, and to convey the resulting work. The terms of this
  558 +License will continue to apply to the part which is the covered work,
  559 +but the special requirements of the GNU Affero General Public License,
  560 +section 13, concerning interaction through a network will apply to the
  561 +combination as such.
  562 +
  563 + 14. Revised Versions of this License.
  564 +
  565 + The Free Software Foundation may publish revised and/or new versions of
  566 +the GNU General Public License from time to time. Such new versions will
  567 +be similar in spirit to the present version, but may differ in detail to
  568 +address new problems or concerns.
  569 +
  570 + Each version is given a distinguishing version number. If the
  571 +Program specifies that a certain numbered version of the GNU General
  572 +Public License "or any later version" applies to it, you have the
  573 +option of following the terms and conditions either of that numbered
  574 +version or of any later version published by the Free Software
  575 +Foundation. If the Program does not specify a version number of the
  576 +GNU General Public License, you may choose any version ever published
  577 +by the Free Software Foundation.
  578 +
  579 + If the Program specifies that a proxy can decide which future
  580 +versions of the GNU General Public License can be used, that proxy's
  581 +public statement of acceptance of a version permanently authorizes you
  582 +to choose that version for the Program.
  583 +
  584 + Later license versions may give you additional or different
  585 +permissions. However, no additional obligations are imposed on any
  586 +author or copyright holder as a result of your choosing to follow a
  587 +later version.
  588 +
  589 + 15. Disclaimer of Warranty.
  590 +
  591 + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
  592 +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
  593 +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
  594 +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
  595 +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  596 +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
  597 +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
  598 +ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
  599 +
  600 + 16. Limitation of Liability.
  601 +
  602 + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
  603 +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
  604 +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
  605 +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
  606 +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
  607 +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
  608 +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
  609 +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
  610 +SUCH DAMAGES.
  611 +
  612 + 17. Interpretation of Sections 15 and 16.
  613 +
  614 + If the disclaimer of warranty and limitation of liability provided
  615 +above cannot be given local legal effect according to their terms,
  616 +reviewing courts shall apply local law that most closely approximates
  617 +an absolute waiver of all civil liability in connection with the
  618 +Program, unless a warranty or assumption of liability accompanies a
  619 +copy of the Program in return for a fee.
  620 +
  621 + END OF TERMS AND CONDITIONS
  622 +
  623 + How to Apply These Terms to Your New Programs
  624 +
  625 + If you develop a new program, and you want it to be of the greatest
  626 +possible use to the public, the best way to achieve this is to make it
  627 +free software which everyone can redistribute and change under these terms.
  628 +
  629 + To do so, attach the following notices to the program. It is safest
  630 +to attach them to the start of each source file to most effectively
  631 +state the exclusion of warranty; and each file should have at least
  632 +the "copyright" line and a pointer to where the full notice is found.
  633 +
  634 + <one line to give the program's name and a brief idea of what it does.>
  635 + Copyright (C) <year> <name of author>
  636 +
  637 + This program is free software: you can redistribute it and/or modify
  638 + it under the terms of the GNU General Public License as published by
  639 + the Free Software Foundation, either version 3 of the License, or
  640 + (at your option) any later version.
  641 +
  642 + This program is distributed in the hope that it will be useful,
  643 + but WITHOUT ANY WARRANTY; without even the implied warranty of
  644 + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  645 + GNU General Public License for more details.
  646 +
  647 + You should have received a copy of the GNU General Public License
  648 + along with this program. If not, see <http://www.gnu.org/licenses/>.
  649 +
  650 +Also add information on how to contact you by electronic and paper mail.
  651 +
  652 + If the program does terminal interaction, make it output a short
  653 +notice like this when it starts in an interactive mode:
  654 +
  655 + <program> Copyright (C) <year> <name of author>
  656 + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
  657 + This is free software, and you are welcome to redistribute it
  658 + under certain conditions; type `show c' for details.
  659 +
  660 +The hypothetical commands `show w' and `show c' should show the appropriate
  661 +parts of the General Public License. Of course, your program's commands
  662 +might be different; for a GUI interface, you would use an "about box".
  663 +
  664 + You should also get your employer (if you work as a programmer) or school,
  665 +if any, to sign a "copyright disclaimer" for the program, if necessary.
  666 +For more information on this, and how to apply and follow the GNU GPL, see
  667 +<http://www.gnu.org/licenses/>.
  668 +
  669 + The GNU General Public License does not permit incorporating your program
  670 +into proprietary programs. If your program is a subroutine library, you
  671 +may consider it more useful to permit linking proprietary applications with
  672 +the library. If this is what you want to do, use the GNU Lesser General
  673 +Public License instead of this License. But first, please read
  674 +<http://www.gnu.org/philosophy/why-not-lgpl.html>.
vendor/plugins/doc_browser/README 0 → 100644
@@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
  1 += doc_browser plugin for Rails
  2 +
  3 +Do you also think that's boring to browse your filesystem to reach the
  4 +documentation installed on your Rails application? Your probleams are over.
  5 +
  6 +Install doc_browser plugin and browse your documentation from Rails'
  7 +development server.
  8 +
  9 +== Usage
  10 +
  11 +Install the plugin:
  12 +
  13 + ./script/plugin install https://svn.colivre.coop.br/svn/rails/plugins/doc_browser
  14 +
  15 +The plugin installer will create a symbolic link in the public/ directory in
  16 +your Rails application, pointing to ../doc (your doc directory at the
  17 +application's root). If it doesn't (or if you remove it and decide to add
  18 +again), we can do the following:
  19 +
  20 + cd public; ln -s ../doc
  21 +
  22 +That's it! Now point your browser at http://localhost:3000/doc and see all the
  23 +available documentation listed for you.
  24 +
  25 +*Note:* doc_browser plugin is activated only in development mode. (i.e. when
  26 +<tt>ENV['RAILS_ENV] == 'development'</tt>)
  27 +
  28 +== Knows limitations
  29 +
  30 +* tested only with WEBrick
  31 +
  32 +== Sending Feedback
  33 +
  34 +Send suggestions, bug reports, patches, pizza and beer to
  35 +terceiro@colivre.coop.br
  36 +
  37 +== License
  38 +
  39 +doc_browser - a documentation browser plugin for Rails
  40 +Copyright (C) 2007 Colivre <http://www.colivre.coop.br>
  41 +
  42 +This program is free software: you can redistribute it and/or modify
  43 +it under the terms of the GNU General Public License as published by
  44 +the Free Software Foundation, either version 3 of the License, or
  45 +(at your option) any later version.
  46 +
  47 +This program is distributed in the hope that it will be useful,
  48 +but WITHOUT ANY WARRANTY; without even the implied warranty of
  49 +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  50 +GNU General Public License for more details.
  51 +
  52 +You should have received a copy of the GNU General Public License
  53 +along with this program. If not, see <http://www.gnu.org/licenses/>.
  54 +
  55 +Please see the file <tt>LICENSE</tt> (distributed together with the plugin) for
  56 +the full terms of distribution.
vendor/plugins/doc_browser/Rakefile 0 → 100644
@@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
  1 +require 'rake'
  2 +require 'rake/testtask'
  3 +require 'rake/rdoctask'
  4 +
  5 +desc 'Default: run unit tests.'
  6 +task :default => :test
  7 +
  8 +desc 'Test the doc_browser plugin.'
  9 +Rake::TestTask.new(:test) do |t|
  10 + t.libs << 'lib'
  11 + t.pattern = 'test/**/*_test.rb'
  12 + t.verbose = true
  13 +end
  14 +
  15 +desc 'Generate documentation for the doc_browser plugin.'
  16 +Rake::RDocTask.new(:rdoc) do |rdoc|
  17 + rdoc.rdoc_dir = 'rdoc'
  18 + rdoc.title = 'DocBrowser plugin'
  19 + rdoc.options << '--line-numbers' << '--inline-source'
  20 + rdoc.rdoc_files.include('README')
  21 + rdoc.rdoc_files.include('lib/**/*.rb')
  22 +end
vendor/plugins/doc_browser/controllers/doc_controller.rb 0 → 100644
@@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
  1 +require 'doc_browser'
  2 +
  3 +# Controller for serving documentation installed in a Rails application
  4 +class DocController < ApplicationController
  5 +
  6 + self.template_root = File.join(File.dirname(__FILE__), '..', 'views')
  7 +
  8 + layout 'doc'
  9 +
  10 + def index
  11 + @docs = DocBrowser.find_docs
  12 + @errors = DocBrowser.errors
  13 + end
  14 +
  15 +end
vendor/plugins/doc_browser/init.rb 0 → 100644
@@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
  1 +if ENV['RAILS_ENV'] == 'development'
  2 + controllers_path = File.join(File.dirname(__FILE__), 'controllers')
  3 + $LOAD_PATH << controllers_path
  4 + Dependencies.load_paths << controllers_path
  5 + config.controller_paths << controllers_path
  6 +end
vendor/plugins/doc_browser/install.rb 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 +# Install hook code here
  2 +
  3 +#public_dir = File.join(RAILS_ROOT, 'public')
  4 +#Dir.chdir(public_dir) do |dir|
  5 +# File.symlink('../doc', 'doc')
  6 +#end
  7 +
  8 +#puts "*****************************************"
  9 +#puts "A symbolic link do your doc directory was just created."
  10 +#puts " doc_browser plugins needs it to work."
  11 +#puts "*****************************************"
vendor/plugins/doc_browser/lib/doc_browser.rb 0 → 100644
@@ -0,0 +1,83 @@ @@ -0,0 +1,83 @@
  1 +# doc_browser - a documentation browser plugin for Rails
  2 +# Copyright (C) 2007 Colivre <http://www.colivre.coop.br>
  3 +#
  4 +# This program is free software: you can redistribute it and/or modify
  5 +# it under the terms of the GNU General Public License as published by
  6 +# the Free Software Foundation, either version 3 of the License, or
  7 +# (at your option) any later version.
  8 +#
  9 +# This program is distributed in the hope that it will be useful,
  10 +# but WITHOUT ANY WARRANTY; without even the implied warranty of
  11 +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12 +# GNU General Public License for more details.
  13 +#
  14 +# You should have received a copy of the GNU General Public License
  15 +# along with this program. If not, see <http://www.gnu.org/licenses/>.
  16 +
  17 +# Searches for documentation installed in a Rails application.
  18 +module DocBrowser
  19 +
  20 + # searches for documentation installed in a Rails application. Returns an
  21 + # Array of Hashes with the found docs. Each entry of the array looks like
  22 + # this:
  23 + # {
  24 + # :name => 'name',
  25 + # :title => 'Some descriptive title',
  26 + # :link => 'doc/name/index.html',
  27 + # :doc_exists => true, # in the case the documentation is installed,
  28 + # :dont_exist_message => 'some message' # to be displayed if the doc is not installed
  29 + # }
  30 + def self.find_docs(root = RAILS_ROOT)
  31 + docs = []
  32 +
  33 + # API documentation
  34 + docs << {
  35 + :name => 'api',
  36 + :title => 'Rails API documentation',
  37 + :link => "/doc/api/index.html",
  38 + :doc_exists => File.exists?(File.join(root, 'doc', 'api')),
  39 + :dont_exist_message => 'not present. Run <tt>rake doc:rails</tt> to generate documentation for the Rails API.',
  40 + }
  41 +
  42 + # Application documentation
  43 + docs << {
  44 + :name => 'app',
  45 + :title => 'Application documentation',
  46 + :link => "/doc/app/index.html",
  47 + :doc_exists => File.exists?(File.join(root, 'doc', 'app')),
  48 + :dont_exist_message => 'not present. Run <tt>rake doc:app</tt> to generate documentation for your application.',
  49 + }
  50 +
  51 + Dir.glob(File.join(root, 'vendor', 'plugins', '*')).select do |f|
  52 + File.directory?(f)
  53 + end.map do |dir|
  54 + name = File.basename(dir)
  55 + {
  56 + :name => name,
  57 + :title => "#{name} plugin",
  58 + :link => ("/doc/plugins/#{name}/index.html"),
  59 + :doc_exists => File.exists?(File.join(root, 'doc', 'plugins', name)),
  60 + :dont_exist_message => 'Documentation not generated. Run <tt>rake doc:plugins</tt> to generate documentation for all plugins in your application.',
  61 + }
  62 + end.each do |item|
  63 + docs << item
  64 + end
  65 +
  66 + docs
  67 + end
  68 +
  69 + # checks if there are any errors that may prevent the user to see any
  70 + # documentation. Returns an Array with error messages.
  71 + #
  72 + # An empty Array, of course, means no errors.
  73 + def self.errors(root = RAILS_ROOT)
  74 + errors = []
  75 +
  76 + unless File.exists?(File.join(root, 'public', 'doc'))
  77 + errors << "There is no symbolic link to your doc directory inside your public directory. Documentation links are probably going to be broken (or even point to parts of your application). To fix this, enter your public/ directory and do <tt>ln -s ../doc</tt>"
  78 + end
  79 + errors
  80 + end
  81 +
  82 +end
  83 +
vendor/plugins/doc_browser/tasks/doc_browser_tasks.rake 0 → 100644
@@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
  1 +# desc "Explaining what the task does"
  2 +# task :doc_browser do
  3 +# # Task goes here
  4 +# end
0 \ No newline at end of file 5 \ No newline at end of file
vendor/plugins/doc_browser/test/doc_browser_test.rb 0 → 100644
@@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
  1 +require File.join(File.dirname(__FILE__), 'test_helper')
  2 +require 'test/unit'
  3 +require File.join(File.dirname(__FILE__), '..', 'lib', 'doc_browser')
  4 +
  5 +class DocBroserTest < Test::Unit::TestCase
  6 +
  7 + def roots(name)
  8 + File.join(File.dirname(__FILE__), 'fixtures', name)
  9 + end
  10 +
  11 + def test_should_list_existing_docs
  12 + docs = DocBrowser.find_docs(roots('regular'))
  13 + assert_kind_of Array, docs
  14 + assert_equal 3, docs.size
  15 + end
  16 +
  17 + def test_should_detect_missing_symlink
  18 + errors = DocBrowser.errors(roots('no_symlink'))
  19 + assert(errors.any? do |item|
  20 + item =~ /no symbolic link/
  21 + end)
  22 + end
  23 +
  24 +end
vendor/plugins/doc_browser/test/doc_controller_test.rb 0 → 100644
@@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
  1 +require File.join(File.dirname(__FILE__), 'test_helper')
  2 +require 'test/unit'
  3 +require File.join(File.dirname(__FILE__), '..', 'controllers', 'doc_controller')
  4 +
  5 +class DocController; def rescue_action(e) raise e end; end
  6 +
  7 +class DocControllerTest < Test::Unit::TestCase
  8 +
  9 + def setup
  10 + @controller = DocController.new
  11 + end
  12 +
  13 + def test_index
  14 + @controller.index
  15 + assert_kind_of Array, assigns(:docs)
  16 + assert_kind_of Array, assigns(:errors)
  17 + end
  18 +
  19 +
  20 +end
vendor/plugins/doc_browser/test/fixtures/regular/doc/api/index.html 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +<!-- empty -->
vendor/plugins/doc_browser/test/fixtures/regular/doc/app/index.html 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +<!-- empty -->
vendor/plugins/doc_browser/test/fixtures/regular/doc/plugins/some_plugin/index.html 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +<!-- empty -->
vendor/plugins/doc_browser/test/fixtures/regular/vendor/plugins/some_plugin/init.rb 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +# empty
vendor/plugins/doc_browser/test/test_helper.rb 0 → 100644
@@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
  1 +ENV["RAILS_ENV"] = "test"
  2 +require File.expand_path(File.dirname(__FILE__) + "/../../../../config/environment")
  3 +
  4 +class Test::Unit::TestCase
  5 + protected
  6 + def assigns(sym)
  7 + @controller.instance_variable_get("@#{sym}")
  8 + end
  9 +end
vendor/plugins/doc_browser/uninstall.rb 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +# Uninstall hook code here
vendor/plugins/doc_browser/views/doc/index.rhtml 0 → 100644
@@ -0,0 +1,42 @@ @@ -0,0 +1,42 @@
  1 +<div id='fileHeader'>
  2 + <h1>Rails Documentation Browser</h1>
  3 + A central place for all your Rails documentation
  4 +</div>
  5 +
  6 +<div id='bodyContent'>
  7 + <div id="contextContent">
  8 + <div id="description">
  9 +
  10 + <p>
  11 + Welcome to the documentation browser. This page lists all the documentation you have generated in your application, including Rails API documentation, documentation for your application, and documentation for the installed plugins.
  12 + </p>
  13 + <p>
  14 + Below you'll have a list of links to all the documentations supported. In the case any documentation is not available you'll be given a hint on how you should generate it.
  15 + </p>
  16 +
  17 +<% unless @errors.empty? %>
  18 +<div id='errors' style='color: red; border: 2px solid red; background: #fee; padding-left: 0.5em; padding-right: 0.5em;'>
  19 +<h2>Errors</h2>
  20 +<ul>
  21 + <% @errors.each do |error| %>
  22 + <li class='error'><%= error %></li>
  23 + <% end %>
  24 +</ul>
  25 +</div>
  26 +<% end %>
  27 +
  28 +<ul>
  29 + <% @docs.each do |item| %>
  30 + <li><%= item[:doc_exists] ? (link_to item[:title], item[:link]) : (content_tag('span', item[:name], :class => 'warning') + ': ' + item[:dont_exist_message]) %></li>
  31 + <% end %>
  32 +</ul>
  33 +
  34 +<p>
  35 +If you have any suggestions regarding Rails Documentation Browser, please check
  36 +the <tt>doc_browser plugin</tt> documentation above to see how you can send
  37 +your suggestions, bug reports, and better yet, patches.
  38 +</p>
  39 +
  40 +</div>
  41 +</div>
  42 +</div>
vendor/plugins/doc_browser/views/doc/no_doc_symlink.rhtml 0 → 100644
@@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
  1 +<h1>No doc symlink</h1>
  2 +
  3 +<p>
  4 +Please create a symbolic link to your 'doc' directory inside yout public directory. You can do soeething linke this:</p>
  5 +
  6 +<pre>
  7 +cd public; ln -s ../doc
  8 +</pre>
vendor/plugins/doc_browser/views/layouts/doc.rhtml 0 → 100644
@@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
  1 +<html>
  2 + <head>
  3 + <link rel="stylesheet" href="/doc/api/rdoc-style.css" type="text/css" media="screen" />
  4 + </head>
  5 + <body>
  6 + <%= yield %>
  7 + </body>
  8 +</html>
vendor/plugins/rails_rcov/MIT-LICENSE 0 → 100644
@@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
  1 +Copyright (c) 2006 Coda Hale
  2 +
  3 +Permission is hereby granted, free of charge, to any person obtaining
  4 +a copy of this software and associated documentation files (the
  5 +"Software"), to deal in the Software without restriction, including
  6 +without limitation the rights to use, copy, modify, merge, publish,
  7 +distribute, sublicense, and/or sell copies of the Software, and to
  8 +permit persons to whom the Software is furnished to do so, subject to
  9 +the following conditions:
  10 +
  11 +The above copyright notice and this permission notice shall be
  12 +included in all copies or substantial portions of the Software.
  13 +
  14 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  15 +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  16 +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  17 +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  18 +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  19 +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  20 +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
vendor/plugins/rails_rcov/README 0 → 100644
@@ -0,0 +1,86 @@ @@ -0,0 +1,86 @@
  1 + = rails_rcov plugin for Rails
  2 +
  3 +rails_rcov provides easy-to-use Rake tasks to determine the code coverage of
  4 +your unit, functional, and integration tests using Mauricio Fernandez's rcov
  5 +tool.
  6 +
  7 +== Installation
  8 +
  9 +First, install rcov from Mauricio's web site
  10 +[http://eigenclass.org/hiki.rb?rcov]. Make sure it's on your system path, so
  11 +that typing +rcov+ on the command line actually runs it. THIS PLUGIN DOESN'T DO
  12 +ANYTHING BESIDES GENERATE ERRORS UNLESS YOU INSTALL RCOV FIRST. RCOV CONTAINS
  13 +ALL THE MAGIC, THIS PLUGIN JUST RUNS IT.
  14 +
  15 +Second, install this plugin. If your project is source-controlled by Subversion
  16 +(which it should be, really), the easiest way to install this is via Rails'
  17 +plugin script:
  18 +
  19 + ./script/plugin install -x http://svn.codahale.com/rails_rcov
  20 +
  21 +If you're not using Subversion, or if you don't want it adding
  22 +<tt>svn:externals</tt> in your project, remove the <tt>-x</tt> switch:
  23 +
  24 + ./script/plugin install http://svn.codahale.com/rails_rcov
  25 +
  26 +== Usage
  27 +
  28 +For each <tt>test:blah</tt> task you have for your Rails project, rails_rcov
  29 +adds two more: <tt>test:blah:rcov</tt> and <tt>test:blah:clobber_rcov</tt>.
  30 +
  31 +Running <tt>rake test:units:rcov</tt>, for example, will run your unit tests
  32 +through rcov and write the code coverage reports to
  33 +<tt>your_rails_app/coverage/units</tt>.
  34 +
  35 +Running <tt>test:units:clobber_rcov</tt> will erase the generated report for the
  36 +unit tests.
  37 +
  38 +Each rcov task takes two optional parameters: RCOV_PARAMS, whose argument is
  39 +passed along to rcov, and SHOW_ONLY, which limits the files displayed in the
  40 +report.
  41 +
  42 +RCOV_PARAMS:
  43 + # sort by coverage
  44 + rake test:units:rcov RCOV_PARAMS="--sort=coverage"
  45 +
  46 + # show callsites and hide fully covered files
  47 + rake test:units:rcov RCOV_PARAMS="--callsites --only-uncovered"
  48 +
  49 +Check the rcov documentation for more details.
  50 +
  51 +SHOW_ONLY is a comma-separated list of the files you'd like to see. Right now
  52 +there are four types of files rake_rcov recognizes: models, helpers,
  53 +controllers, and lib. These can be abbreviated to their first letters:
  54 +
  55 + # only show files from app/models
  56 + rake test:units:rcov SHOW_ONLY=models
  57 +
  58 + # only show files from app/helpers and app/controllers
  59 + rake test:units:rcov SHOW_ONLY=helpers,controllers
  60 +
  61 + # only show files from app/helpers and app/controllers, with less typing
  62 + rake test:units:rcov SHOW_ONLY=h,c
  63 +
  64 +Please note that rails_rcov has only been tested with a Bash shell, and any
  65 +other environment could well explode in your face. If you're having trouble
  66 +getting this to work on Windows, please take the time to figure out what's not
  67 +working. Most of the time it boils down to the different ways the Window shell
  68 +and the Bash shell escape metacharacters. Play around with the way rcov_rake
  69 +escapes data (like on line 73, or 78) and send me a fix. I don't have a working
  70 +Windows environment anymore, so leaving it up to me won't solve anything. ;-)
  71 +
  72 +== Resources
  73 +
  74 +=== Subversion
  75 +
  76 +* http://svn.codahale.com/rails_rcov
  77 +
  78 +=== Blog
  79 +
  80 +* http://blog.codahale.com
  81 +
  82 +== Credits
  83 +
  84 +Written by Coda Hale <coda.hale@gmail.com>. Thanks to Nils Franzen for a Win32
  85 +escaping patch. Thanks to Alex Wayne for suggesting how to make SHOW_ONLY not be
  86 +useless.
0 \ No newline at end of file 87 \ No newline at end of file
vendor/plugins/rails_rcov/tasks/rails_rcov.rake 0 → 100644
@@ -0,0 +1,154 @@ @@ -0,0 +1,154 @@
  1 +# This File Uses Magic
  2 +# ====================
  3 +# Here's an example of how this file works. As an example, let's say you typed
  4 +# this into your terminal:
  5 +#
  6 +# $ rake --tasks
  7 +#
  8 +# The rake executable goes through all the various places .rake files can be,
  9 +# accumulates them all, and then runs them. When this file is loaded by Rake,
  10 +# it iterates through all the tasks, and for each task named 'test:blah' adds
  11 +# test:blah:rcov and test:blah:rcov_clobber.
  12 +#
  13 +# So you've seen all the tasks, and you type this into your terminal:
  14 +#
  15 +# $ rake test:units:rcov
  16 +#
  17 +# Rake does the same thing as above, but it runs the test:units:rcov task, which
  18 +# pretty much just does this:
  19 +#
  20 +# $ ruby [this file] [the test you want to run] [some options]
  21 +#
  22 +# Now this file is run via the Ruby interpreter, and after glomming up the
  23 +# options, it acts just like the Rake executable, with a slight difference: it
  24 +# passes all the arguments to rcov, not ruby, so all your unit tests get some
  25 +# rcov sweet loving.
  26 +
  27 +if ARGV.grep(/--run-rake-task=/).empty?
  28 + # Define all our Rake tasks
  29 +
  30 + require 'rake/clean'
  31 + require 'rcov/rcovtask'
  32 +
  33 + def to_rcov_task_sym(s)
  34 + s = s.gsub(/(test:)/,'')
  35 + s.empty? ? nil : s.intern
  36 + end
  37 +
  38 + def to_rcov_task_name(s)
  39 + s = s.gsub(/(test:)/,'')
  40 + s =~ /s$/i ? s[0..-2] : s
  41 + end
  42 +
  43 + def new_rcov_task(test_name)
  44 + output_dir = "./coverage/#{test_name.gsub('test:','')}"
  45 + CLOBBER.include(output_dir)
  46 +
  47 + # Add a task to run the rcov process
  48 + desc "Run all #{to_rcov_task_name(test_name)} tests with Rcov to measure coverage"
  49 + task :rcov => [:clobber_rcov] do |t|
  50 + run_code = '"' << File.expand_path(__FILE__) << '"'
  51 + run_code << " --run-rake-task=#{test_name}"
  52 +
  53 + params = String.new
  54 + if ENV['RCOV_PARAMS']
  55 + params << ENV['RCOV_PARAMS']
  56 + end
  57 +
  58 + # rake test:units:rcov SHOW_ONLY=models,controllers,lib,helpers
  59 + # rake test:units:rcov SHOW_ONLY=m,c,l,h
  60 + if ENV['SHOW_ONLY']
  61 + show_only = ENV['SHOW_ONLY'].to_s.split(',').map{|x|x.strip}
  62 + if show_only.any?
  63 + reg_exp = []
  64 + for show_type in show_only
  65 + reg_exp << case show_type
  66 + when 'm', 'models' : 'app\/models'
  67 + when 'c', 'controllers' : 'app\/controllers'
  68 + when 'h', 'helpers' : 'app\/helpers'
  69 + when 'l', 'lib' : 'lib'
  70 + else
  71 + show_type
  72 + end
  73 + end
  74 + reg_exp.map!{ |m| "(#{m})" }
  75 + params << " -x \\\"^(?!#{reg_exp.join('|')})\\\""
  76 + end
  77 + end
  78 +
  79 + unless params.empty?
  80 + run_code << " --rcov-params=\"#{params}\""
  81 + end
  82 +
  83 + ruby run_code
  84 + end
  85 +
  86 + # Add a task to clean up after ourselves
  87 + desc "Remove Rcov reports for #{to_rcov_task_name(test_name)} tests"
  88 + task :clobber_rcov do |t|
  89 + rm_r output_dir, :force => true
  90 + end
  91 +
  92 + # Link our clobber task to the main one
  93 + task :clobber => [:clobber_rcov]
  94 + end
  95 +
  96 + test_tasks = Rake::Task.tasks.select{ |t| t.comment && t.name =~ /^test/ }
  97 + for test_task in test_tasks
  98 + namespace :test do
  99 + if sym = to_rcov_task_sym(test_task.name)
  100 + namespace sym do
  101 + new_rcov_task(test_task.name)
  102 + end
  103 + end
  104 + end
  105 + end
  106 +else
  107 + # Load rake tasks, hijack ruby, and redirect the task through rcov
  108 + begin
  109 + require 'rubygems'
  110 + rescue LoadError
  111 + # don't force people to use rubygems
  112 + end
  113 + require 'rake'
  114 +
  115 + module RcovTestSettings
  116 + class << self
  117 + attr_accessor :output_dir, :options
  118 + def to_params
  119 + "-o \"#{@output_dir}\" -T -x \"rubygems/*,rcov*\" --rails #{@options}"
  120 + end
  121 + end
  122 +
  123 + # load options and arguments from command line
  124 + unless (cmd_line = ARGV.grep(/--rcov-params=/)).empty?
  125 + @options = cmd_line.first.gsub(/--rcov-params=/, '')
  126 + end
  127 + end
  128 +
  129 + def is_windows?
  130 + processor, platform, *rest = RUBY_PLATFORM.split("-")
  131 + platform == 'mswin32'
  132 + end
  133 +
  134 + # intercept what Rake *would* be doing with Ruby, and send it through Rcov instead
  135 + module RakeFileUtils
  136 + alias :ruby_without_rcov :ruby
  137 + def ruby(*args, &block)
  138 + cmd = (is_windows? ? 'rcov.cmd' : 'rcov') << " #{RcovTestSettings.to_params} #{args}"
  139 + status = sh(cmd, {}, &block)
  140 + puts "View the full results at <file://#{RcovTestSettings.output_dir}/index.html>"
  141 + return status
  142 + end
  143 + end
  144 +
  145 + # read the test name and execute it (through Rcov)
  146 + unless (cmd_line = ARGV.grep(/--run-rake-task=/)).empty?
  147 + test_name = cmd_line.first.gsub(/--run-rake-task=/,'')
  148 + ARGV.clear; ARGV << test_name
  149 + RcovTestSettings.output_dir = File.expand_path("./coverage/#{test_name.gsub('test:','')}")
  150 + Rake.application.run
  151 + else
  152 + raise "No test to execute!"
  153 + end
  154 +end