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

Too many changes.

To preserve performance only 100 of 154 files displayed.

vendor/plugins/access_control/README 0 → 100644
... ... @@ -0,0 +1,4 @@
  1 +AccessControl
  2 +=============
  3 +
  4 +Description goes here
0 5 \ No newline at end of file
... ...
vendor/plugins/access_control/Rakefile 0 → 100644
... ... @@ -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 @@
  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 @@
  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 @@
  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 @@
  1 +# Install hook code here
... ...
vendor/plugins/access_control/lib/access_control.rb 0 → 100644
... ... @@ -0,0 +1 @@
  1 +# AccessControl
0 2 \ No newline at end of file
... ...
vendor/plugins/access_control/lib/acts_as_accessible.rb 0 → 100644
... ... @@ -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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  1 +# desc "Explaining what the task does"
  2 +# task :access_control do
  3 +# # Task goes here
  4 +# end
0 5 \ No newline at end of file
... ...
vendor/plugins/access_control/test/access_control_test.rb 0 → 100644
... ... @@ -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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  1 +# Uninstall hook code here
... ...
vendor/plugins/access_control/views/access_denied.rhtml 0 → 100644
... ... @@ -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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  1 +module TagCountsExtension
  2 +end
... ...
vendor/plugins/acts_as_taggable_on_steroids/lib/tag_list.rb 0 → 100644
... ... @@ -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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 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 @@
  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 @@
  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 @@
  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 20 \ No newline at end of file
... ...
vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/user.rb 0 → 100644
... ... @@ -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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 75 \ No newline at end of file
... ...
vendor/plugins/acts_as_versioned/MIT-LICENSE 0 → 100644
... ... @@ -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 21 \ No newline at end of file
... ...
vendor/plugins/acts_as_versioned/README 0 → 100644
... ... @@ -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 29 \ No newline at end of file
... ...
vendor/plugins/acts_as_versioned/RUNNING_UNIT_TESTS 0 → 100644
... ... @@ -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 @@
  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 183 \ No newline at end of file
... ...
vendor/plugins/acts_as_versioned/init.rb 0 → 100644
... ... @@ -0,0 +1 @@
  1 +require 'acts_as_versioned'
0 2 \ No newline at end of file
... ...
vendor/plugins/acts_as_versioned/lib/acts_as_versioned.rb 0 → 100644
... ... @@ -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 546 \ No newline at end of file
... ...
vendor/plugins/acts_as_versioned/test/abstract_unit.rb 0 → 100644
... ... @@ -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 42 \ No newline at end of file
... ...
vendor/plugins/acts_as_versioned/test/database.yml 0 → 100644
... ... @@ -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 19 \ No newline at end of file
... ...
vendor/plugins/acts_as_versioned/test/fixtures/authors.yml 0 → 100644
... ... @@ -0,0 +1,6 @@
  1 +caged:
  2 + id: 1
  3 + name: caged
  4 +mly:
  5 + id: 2
  6 + name: mly
0 7 \ No newline at end of file
... ...
vendor/plugins/acts_as_versioned/test/fixtures/landmark.rb 0 → 100644
... ... @@ -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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 16 \ No newline at end of file
... ...
vendor/plugins/acts_as_versioned/test/fixtures/page.rb 0 → 100644
... ... @@ -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 44 \ No newline at end of file
... ...
vendor/plugins/acts_as_versioned/test/fixtures/page_versions.yml 0 → 100644
... ... @@ -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 @@
  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 8 \ No newline at end of file
... ...
vendor/plugins/acts_as_versioned/test/fixtures/widget.rb 0 → 100644
... ... @@ -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 7 \ No newline at end of file
... ...
vendor/plugins/acts_as_versioned/test/migration_test.rb 0 → 100644
... ... @@ -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 @@
  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 @@
  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 329 \ No newline at end of file
... ...
vendor/plugins/attachment_fu/CHANGELOG 0 → 100644
... ... @@ -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 20 \ No newline at end of file
... ...