diff --git a/vendor/plugins/access_control/README b/vendor/plugins/access_control/README new file mode 100644 index 0000000..b95281c --- /dev/null +++ b/vendor/plugins/access_control/README @@ -0,0 +1,4 @@ +AccessControl +============= + +Description goes here \ No newline at end of file diff --git a/vendor/plugins/access_control/Rakefile b/vendor/plugins/access_control/Rakefile new file mode 100644 index 0000000..40c77b4 --- /dev/null +++ b/vendor/plugins/access_control/Rakefile @@ -0,0 +1,22 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the access_control plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the access_control plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'AccessControl' + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/vendor/plugins/access_control/generators/access_control_migration/access_control_migration_generator.rb b/vendor/plugins/access_control/generators/access_control_migration/access_control_migration_generator.rb new file mode 100644 index 0000000..76b21b9 --- /dev/null +++ b/vendor/plugins/access_control/generators/access_control_migration/access_control_migration_generator.rb @@ -0,0 +1,12 @@ +class AccessControlMigrationGenerator < Rails::Generator::Base + def manifest + record do |m| + m.migration_template 'migration.rb', 'db/migrate' + end + end + + def file_name + "access_control_migration" + end +end + diff --git a/vendor/plugins/access_control/generators/access_control_migration/templates/migration.rb b/vendor/plugins/access_control/generators/access_control_migration/templates/migration.rb new file mode 100644 index 0000000..26058ec --- /dev/null +++ b/vendor/plugins/access_control/generators/access_control_migration/templates/migration.rb @@ -0,0 +1,22 @@ +class AccessControlMigration < ActiveRecord::Migration + def self.up + create_table :roles do |t| + t.column :name, :string + t.column :permissions, :string + end + + create_table :role_assignments do |t| + t.column :accessor_id, :integer + t.column :accessor_type, :string + t.column :resource_id, :integer + t.column :resource_type, :string + t.column :role_id, :integer + t.column :is_global, :boolean + end + end + + def self.down + drop_table :roles + drop_table :role_assignments + end +end diff --git a/vendor/plugins/access_control/init.rb b/vendor/plugins/access_control/init.rb new file mode 100644 index 0000000..d3e6ddc --- /dev/null +++ b/vendor/plugins/access_control/init.rb @@ -0,0 +1,6 @@ +require 'acts_as_accessor' +require 'acts_as_accessible' +require 'permission_name_helper' +module ApplicationHelper + include PermissionName +end diff --git a/vendor/plugins/access_control/install.rb b/vendor/plugins/access_control/install.rb new file mode 100644 index 0000000..f7732d3 --- /dev/null +++ b/vendor/plugins/access_control/install.rb @@ -0,0 +1 @@ +# Install hook code here diff --git a/vendor/plugins/access_control/lib/access_control.rb b/vendor/plugins/access_control/lib/access_control.rb new file mode 100644 index 0000000..1e5a5b1 --- /dev/null +++ b/vendor/plugins/access_control/lib/access_control.rb @@ -0,0 +1 @@ +# AccessControl \ No newline at end of file diff --git a/vendor/plugins/access_control/lib/acts_as_accessible.rb b/vendor/plugins/access_control/lib/acts_as_accessible.rb new file mode 100644 index 0000000..22b2cc9 --- /dev/null +++ b/vendor/plugins/access_control/lib/acts_as_accessible.rb @@ -0,0 +1,26 @@ +class ActiveRecord::Base + # This is the global hash of permissions and each item is of the form + # 'class_name' => permission_hash for each target have its own set of permissions + # but its not a namespace so each permission name should be unique + PERMISSIONS = {} + + # Acts as accessible makes a model acts as a resource that can be targeted by a permission + def self.acts_as_accessible + has_many :role_assignments, :as => :resource + + # A superior instance is an object that has higher level an thus can be targeted by a permission + # to represent an permission over a group of related resources rather than a single one + def superior_instance + nil + end + + def affiliate(accessor, roles) + roles = [roles] unless roles.kind_of?(Array) + roles.map {|role| accessor.add_role(role, self)}.any? + end + + def members + role_assignments.map(&:accessor).uniq + end + end +end diff --git a/vendor/plugins/access_control/lib/acts_as_accessor.rb b/vendor/plugins/access_control/lib/acts_as_accessor.rb new file mode 100644 index 0000000..0ab513e --- /dev/null +++ b/vendor/plugins/access_control/lib/acts_as_accessor.rb @@ -0,0 +1,58 @@ +class ActiveRecord::Base + def self.acts_as_accessor + has_many :role_assignments, :as => :accessor + + def has_permission?(permission, resource = nil) + return true if resource == self + role_assignments.any? {|ra| ra.has_permission?(permission, resource)} + end + + def define_roles(roles, resource) + roles = [roles] unless roles.kind_of?(Array) + actual_roles = RoleAssignment.find( :all, :conditions => role_attributes(nil, resource) ).map(&:role) + + (roles - actual_roles).each {|r| add_role(r, resource) } + (actual_roles - roles).each {|r| remove_role(r, resource)} + end + + def add_role(role, resource) + attributes = role_attributes(role, resource) + if RoleAssignment.find(:all, :conditions => attributes).empty? + RoleAssignment.new(attributes).save + else + false + end + end + + def remove_role(role, resource) + return unless role + roles_destroy = RoleAssignment.find(:all, :conditions => role_attributes(role, resource)) + return if roles_destroy.empty? + roles_destroy.map(&:destroy).all? + end + + def find_roles(res) + RoleAssignment.find(:all, :conditions => role_attributes(nil, res)) + end + + protected + def role_attributes(role, resource) + attributes = {:accessor_id => self.id, :accessor_type => self.class.base_class.name} + if role + attributes[:role_id] = role.id + end + if resource == 'global' + attributes[:is_global] = true + resource = nil + end + if resource + attributes[:resource_id] = resource.id + attributes[:resource_type] = resource.class.base_class.name + else + attributes[:resource_id] = nil + attributes[:resource_type] = nil + end + attributes + end + end +end diff --git a/vendor/plugins/access_control/lib/permission_check.rb b/vendor/plugins/access_control/lib/permission_check.rb new file mode 100644 index 0000000..fbf3ba3 --- /dev/null +++ b/vendor/plugins/access_control/lib/permission_check.rb @@ -0,0 +1,41 @@ +module PermissionCheck + + module ClassMethods + # Declares the +permission+ need to be able to access +action+. + # + # * +permission+ must be a symbol or string naming the needed permission to + # access the specified actions. + # * +target+ is the object over witch the user would need the specified + # permission and must be specified as a symbol or the string 'global'. The controller using + # +target+ must respond to a method with that name returning the object + # against which the permissions needed will be checked or if 'global' is passed it will be + # cheked if the assignment is global + # * +accessor+ is a mehtod that returns the accessor who must have the permission. By default + # is :user + # * +action+ must be a hash of options for a before filter like + # :only => :index or :except => [:edit, :update] by default protects all the actions + def protect(permission, target_method, accessor_method = :user, actions = {}) + actions, accessor_method = accessor_method, :user if accessor_method.kind_of?(Hash) + before_filter actions do |c| + target = target_method.kind_of?(Symbol) ? c.send(target_method) : target_method + accessor = accessor_method.kind_of?(Symbol) ? c.send(accessor_method) : accessor_method + unless accessor && accessor.has_permission?(permission.to_s, target) +# c.instance_variable_set('@b', [accessor, permission, target]) + c.send(:render, :file => access_denied_template_path, :status => 403) && false + end + end + end + + def access_denied_template_path + if File.exists?(File.join(RAILS_ROOT, 'app', 'views','access_control' ,'access_denied.rhtml')) + file_path = File.join(RAILS_ROOT, 'app', 'views','access_control' ,'access_denied.rhtml') + else + file_path = File.join(File.dirname(__FILE__),'..', 'views','access_denied.rhtml') + end + end + end + + def self.included(including) + including.send(:extend, PermissionCheck::ClassMethods) + end +end diff --git a/vendor/plugins/access_control/lib/permission_name_helper.rb b/vendor/plugins/access_control/lib/permission_name_helper.rb new file mode 100644 index 0000000..b67f02e --- /dev/null +++ b/vendor/plugins/access_control/lib/permission_name_helper.rb @@ -0,0 +1,6 @@ +module PermissionName + def permission_name(p) + msgid = ActiveRecord::Base::PERMISSIONS.values.inject({}){|s,v| s.merge(v)}[p] + gettext(msgid) + end +end diff --git a/vendor/plugins/access_control/lib/role.rb b/vendor/plugins/access_control/lib/role.rb new file mode 100644 index 0000000..fb83f99 --- /dev/null +++ b/vendor/plugins/access_control/lib/role.rb @@ -0,0 +1,29 @@ +class Role < ActiveRecord::Base + + has_many :role_assignments + serialize :permissions, Array + validates_presence_of :name + validates_uniqueness_of :name + + def initialize(*args) + super(*args) + self[:permissions] ||= [] + end + + def has_permission?(perm) + permissions.include?(perm) + end + + def has_kind?(k) + permissions.any?{|p| perms[k].keys.include?(p)} + end + + def kind + perms.keys.detect{|k| perms[k].keys.include?(permissions[0]) } + end + + protected + def perms + ActiveRecord::Base::PERMISSIONS + end +end diff --git a/vendor/plugins/access_control/lib/role_assignment.rb b/vendor/plugins/access_control/lib/role_assignment.rb new file mode 100644 index 0000000..828ea8c --- /dev/null +++ b/vendor/plugins/access_control/lib/role_assignment.rb @@ -0,0 +1,18 @@ +class RoleAssignment < ActiveRecord::Base + belongs_to :role + belongs_to :accessor, :polymorphic => true + belongs_to :resource, :polymorphic => true + + validates_presence_of :role, :accessor + + def has_permission?(perm, res) + return false unless role.has_permission?(perm.to_s) && (resource || is_global) + return true if is_global + return false if res == 'global' + while res + return true if (resource == res) + res = res.superior_instance + end + return (resource == res) + end +end diff --git a/vendor/plugins/access_control/tasks/access_control_tasks.rake b/vendor/plugins/access_control/tasks/access_control_tasks.rake new file mode 100644 index 0000000..83d053f --- /dev/null +++ b/vendor/plugins/access_control/tasks/access_control_tasks.rake @@ -0,0 +1,4 @@ +# desc "Explaining what the task does" +# task :access_control do +# # Task goes here +# end \ No newline at end of file diff --git a/vendor/plugins/access_control/test/access_control_test.rb b/vendor/plugins/access_control/test/access_control_test.rb new file mode 100644 index 0000000..e905578 --- /dev/null +++ b/vendor/plugins/access_control/test/access_control_test.rb @@ -0,0 +1,4 @@ +require 'test/unit' + +class AccessControlTest < Test::Unit::TestCase +end diff --git a/vendor/plugins/access_control/test/acts_as_accessible_test.rb b/vendor/plugins/access_control/test/acts_as_accessible_test.rb new file mode 100644 index 0000000..7237c36 --- /dev/null +++ b/vendor/plugins/access_control/test/acts_as_accessible_test.rb @@ -0,0 +1,12 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/test_helper' + +class AccessControlTest < Test::Unit::TestCase + def test_can_have_role_in_respect_to_an_resource + r = AccessControlTestResource.create(:name => 'bla') + a = AccessControlTestAccessor.create(:name => 'ze') + member_role = Role.create(:name => 'member', :permissions => ['bli']) + r.affiliate(a, member_role) + assert a.has_permission?('bli', r) + end +end diff --git a/vendor/plugins/access_control/test/acts_as_accessor_test.rb b/vendor/plugins/access_control/test/acts_as_accessor_test.rb new file mode 100644 index 0000000..5fe1c9d --- /dev/null +++ b/vendor/plugins/access_control/test/acts_as_accessor_test.rb @@ -0,0 +1,58 @@ +require 'test/unit' +require File.dirname(__FILE__) + '/test_helper' + +class ActAsAccessorTest < Test::Unit::TestCase + def test_can_have_role_in_respect_to_an_resource + res = AccessControlTestResource.create!(:name => 'bla') + a = AccessControlTestAccessor.create!(:name => 'ze') + role = Role.create!(:name => 'just_a_member', :permissions => ['bli']) + assert a.add_role(role, res) + assert a.has_permission?('bli', res) + end + + def test_can_have_a_global_role + r = AccessControlTestResource.create!(:name => 'bla') + a = AccessControlTestAccessor.create!(:name => 'ze') + member_role = Role.create!(:name => 'just_a_moderator', :permissions => ['bli']) + assert a.add_role(member_role, 'global') + assert a.has_permission?('bli', 'global') + end + + def test_add_role + res = AccessControlTestResource.create!(:name => 'bla') + a = AccessControlTestAccessor.create!(:name => 'ze') + role = Role.create!(:name => 'just_a_content_author', :permissions => ['bli']) + assert a.add_role(role, res) + assert a.role_assignments.map{|ra|[ra.role, ra.accessor, ra.resource]}.include?([role, a, res]) + end + + def test_remove_role + res = AccessControlTestResource.create!(:name => 'bla') + a = AccessControlTestAccessor.create!(:name => 'ze') + role = Role.create!(:name => 'just_an_author', :permissions => ['bli']) + ra = RoleAssignment.create!(:accessor => a, :role => role, :resource => res) + + assert a.role_assignments.include?(ra) + assert a.remove_role(role, res) + a.reload + assert !a.role_assignments.map{|ra|[ra.role, ra.accessor, ra.resource]}.include?([role, a, res]) + end + + def test_do_not_add_role_twice + res = AccessControlTestResource.create!(:name => 'bla') + a = AccessControlTestAccessor.create!(:name => 'ze') + role = Role.create!(:name => 'a_content_author', :permissions => ['bli']) + assert a.add_role(role, res) + assert !a.add_role(role, res) + assert a.role_assignments.map{|ra|[ra.role, ra.accessor, ra.resource]}.include?([role, a, res]) + end + + def test_do_not_remove_inexistent_role + res = AccessControlTestResource.create!(:name => 'bla') + a = AccessControlTestAccessor.create!(:name => 'ze') + role = Role.create!(:name => 'an_author', :permissions => ['bli']) + + assert !a.role_assignments.map{|ra|[ra.role, ra.accessor, ra.resource]}.include?([role, a, res]) + assert !a.remove_role(role, res) + end +end diff --git a/vendor/plugins/access_control/test/permission_check_test.rb b/vendor/plugins/access_control/test/permission_check_test.rb new file mode 100644 index 0000000..2c01de6 --- /dev/null +++ b/vendor/plugins/access_control/test/permission_check_test.rb @@ -0,0 +1,41 @@ +require File.join(File.dirname(__FILE__), 'test_helper') + +class AccessControlTestController; def rescue_action(e) raise e end; end +class PermissionCheckTest < Test::Unit::TestCase + + def setup + @controller = AccessControlTestController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_access_denied + get :index + assert_response 403 + assert_template 'access_denied.rhtml' + end + + def test_global_permission_granted + user = AccessControlTestAccessor.create!(:name => 'user') + role = Role.create!(:name => 'some_role', :permissions => ['see_index']) + assert user.add_role(role, 'global') + assert user.has_permission?('see_index', 'global') + + get :index, :user => user.id + assert_response :success + assert_template nil + end + + def test_specific_permission_granted + user = AccessControlTestAccessor.create!(:name => 'other_user') + role = Role.create!(:name => 'other_role', :permissions => ['do_some_stuff']) + resource = AccessControlTestResource.create!(:name => 'some_resource') + assert user.add_role(role, resource) + assert user.has_permission?('do_some_stuff', resource) + + get :other_stuff, :user => user.id, :resource => resource.id + assert_response :success + assert_template nil + + end +end diff --git a/vendor/plugins/access_control/test/role_assignment_test.rb b/vendor/plugins/access_control/test/role_assignment_test.rb new file mode 100644 index 0000000..75d6ff7 --- /dev/null +++ b/vendor/plugins/access_control/test/role_assignment_test.rb @@ -0,0 +1,32 @@ +require File.dirname(__FILE__) + '/test_helper' + +class RoleAssignmentTest < Test::Unit::TestCase + + def test_has_global_permission + role = Role.create(:name => 'new_role', :permissions => ['permission']) + ra = RoleAssignment.create(:role => role, :is_global => true) + assert ra.has_permission?('permission', 'global') + assert !ra.has_permission?('not_permitted', 'global') + end + + def test_has_global_permission_with_global_resource + role = Role.create(:name => 'new_role', :permissions => ['permission']) + ra = RoleAssignment.create(:role => role, :is_global => true) + assert ra.has_permission?('permission', 'global') + assert !ra.has_permission?('not_permitted', 'global') + end + + def test_has_specific_permission + role = Role.create(:name => 'new_role', :permissions => ['permission']) + accessor = AccessControlTestAccessor.create(:name => 'accessor') + resource_A = AccessControlTestResource.create(:name => 'Resource A') + resource_B = AccessControlTestResource.create(:name => 'Resource B') + ra = RoleAssignment.create(:accessor => accessor, :role => role, :resource => resource_A) + assert !ra.new_record? + assert_equal role, ra.role + assert_equal accessor, ra.accessor + assert_equal resource_A, ra.resource + assert ra.has_permission?('permission', resource_A) + assert !ra.has_permission?('permission', resource_B) + end +end diff --git a/vendor/plugins/access_control/test/role_test.rb b/vendor/plugins/access_control/test/role_test.rb new file mode 100644 index 0000000..6e4ada3 --- /dev/null +++ b/vendor/plugins/access_control/test/role_test.rb @@ -0,0 +1,28 @@ +require File.join(File.dirname(__FILE__), 'test_helper') + + +class RoleTest < Test::Unit::TestCase + + def test_role_creation + count = Role.count + role = Role.new(:name => 'any_role') + assert role.save + assert_equal count + 1, Role.count + end + + def test_uniqueness_of_name + Role.create(:name => 'role_name') + role = Role.new(:name => 'role_name') + assert ! role.save + end + + def test_permission_setting + role = Role.new(:name => 'permissive_role', :permissions => ['edit_profile']) + assert role.save + assert role.has_permission?('edit_profile') + role.permissions << 'post_content' + assert role.save + assert role.has_permission?('post_content') + assert role.has_permission?('edit_profile') + end +end diff --git a/vendor/plugins/access_control/test/schema.rb b/vendor/plugins/access_control/test/schema.rb new file mode 100644 index 0000000..bf320ae --- /dev/null +++ b/vendor/plugins/access_control/test/schema.rb @@ -0,0 +1,31 @@ +ActiveRecord::Migration.verbose = false + +ActiveRecord::Schema.define(:version => 0) do + + create_table :access_control_test_roles, :force => true do |t| + t.column :name, :string + t.column :permissions, :string + end + + create_table :access_control_test_role_assignments, :force => true do |t| + t.column :role_id, :integer + t.column :accessor_id, :integer + t.column :accessor_type, :string + t.column :resource_id, :integer + t.column :resource_type, :string + t.column :is_global, :boolean + end + + create_table :access_control_test_accessors, :force => true do |t| + t.column :name, :string + end + + create_table :access_control_test_resources, :force => true do |t| + t.column :name, :string + end +end + +ActiveRecord::Migration.verbose = true + + + diff --git a/vendor/plugins/access_control/test/test_helper.rb b/vendor/plugins/access_control/test/test_helper.rb new file mode 100644 index 0000000..dccb906 --- /dev/null +++ b/vendor/plugins/access_control/test/test_helper.rb @@ -0,0 +1,51 @@ +ENV["RAILS_ENV"] = "test" +require File.expand_path(File.dirname(__FILE__) + "/../../../../config/environment") + +require 'test/unit' +require 'mocha' + +# from Rails +require 'test_help' + +# load the database schema for the tests +ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log") +load(File.dirname(__FILE__) + '/schema.rb') +# change the table names for the tests to not touch +Role.set_table_name 'access_control_test_roles' +RoleAssignment.set_table_name 'access_control_test_role_assignments' + +# accessor example class to access some resources +class AccessControlTestAccessor < ActiveRecord::Base + set_table_name 'access_control_test_accessors' + acts_as_accessor +end + +# resource example class to be accessed by some accessor +class AccessControlTestResource < ActiveRecord::Base + set_table_name 'access_control_test_resources' + acts_as_accessible + PERMISSIONS[self.class.name] = {'bla' => N_('Bla')} +end + +# controller to test protection +class AccessControlTestController < ApplicationController + include PermissionCheck + protect 'see_index', 'global', :user, :only => :index + protect 'do_some_stuff', :resource, :user, :only => :other_stuff + def index + render :text => 'test controller' + end + + def other_stuff + render :text => 'test stuff' + end + +protected + def user + AccessControlTestAccessor.find(params[:user]) if params[:user] + end + + def resource + AccessControlTestResource.find(params[:resource]) if params[:resource] + end +end diff --git a/vendor/plugins/access_control/uninstall.rb b/vendor/plugins/access_control/uninstall.rb new file mode 100644 index 0000000..9738333 --- /dev/null +++ b/vendor/plugins/access_control/uninstall.rb @@ -0,0 +1 @@ +# Uninstall hook code here diff --git a/vendor/plugins/access_control/views/access_denied.rhtml b/vendor/plugins/access_control/views/access_denied.rhtml new file mode 100644 index 0000000..f8d98f5 --- /dev/null +++ b/vendor/plugins/access_control/views/access_denied.rhtml @@ -0,0 +1,7 @@ +

<%= _('Access denied') %>

+ +<% unless @message.nil? %> +

+ <%= @message %> +

+<% end %> diff --git a/vendor/plugins/acts_as_ferret/LICENSE b/vendor/plugins/acts_as_ferret/LICENSE new file mode 100644 index 0000000..b07e5a5 --- /dev/null +++ b/vendor/plugins/acts_as_ferret/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2006 Kasper Weibel, Jens Kraemer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/plugins/acts_as_ferret/README b/vendor/plugins/acts_as_ferret/README new file mode 100644 index 0000000..2b47744 --- /dev/null +++ b/vendor/plugins/acts_as_ferret/README @@ -0,0 +1,49 @@ += acts_as_ferret + +This ActiveRecord mixin adds full text search capabilities to any Rails model. + +It is heavily based on the original acts_as_ferret plugin done by +Kasper Weibel and a modified version done by Thomas Lockney, which +both can be found on http://ferret.davebalmain.com/trac/wiki/FerretOnRails + +== Installation + +=== Installation inside your Rails project via script/plugin + +script/plugin install svn://projects.jkraemer.net/acts_as_ferret/trunk/plugin/acts_as_ferret + + +=== System-wide installation with Rubygems + +sudo gem install acts_as_ferret + +To use acts_as_ferret in your project, add the following line to your +project's config/environment.rb: + +require 'acts_as_ferret' + + +== Usage + +include the following in your model class (specifiying the fields you want to get indexed): + +acts_as_ferret :fields => [ :title, :description ] + +now you can use ModelClass.find_by_contents(query) to find instances of your model +whose indexed fields match a given query. All query terms are required by default, +but explicit OR queries are possible. This differs from the ferret default, but +imho is the more often needed/expected behaviour (more query terms result in +less results). + +Please see ActsAsFerret::ActMethods#acts_as_ferret for more information. + +== License + +Released under the MIT license. + +== Authors + +* Kasper Weibel Nielsen-Refs (original author) +* Jens Kraemer (current maintainer) + + diff --git a/vendor/plugins/acts_as_ferret/config/ferret_server.yml b/vendor/plugins/acts_as_ferret/config/ferret_server.yml new file mode 100644 index 0000000..6522f12 --- /dev/null +++ b/vendor/plugins/acts_as_ferret/config/ferret_server.yml @@ -0,0 +1,12 @@ +production: + host: ferret.yourdomain.com + port: 9010 + pid_file: log/ferret.pid +development: + host: localhost + port: 9010 + pid_file: log/ferret.pid +test: + host: localhost + port: 9009 + pid_file: log/ferret.pid diff --git a/vendor/plugins/acts_as_ferret/init.rb b/vendor/plugins/acts_as_ferret/init.rb new file mode 100644 index 0000000..5eca621 --- /dev/null +++ b/vendor/plugins/acts_as_ferret/init.rb @@ -0,0 +1,22 @@ +# Copyright (c) 2006 Kasper Weibel Nielsen-Refs, Thomas Lockney, Jens Krämer +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +require 'acts_as_ferret' + diff --git a/vendor/plugins/acts_as_ferret/install.rb b/vendor/plugins/acts_as_ferret/install.rb new file mode 100644 index 0000000..2bbbfc6 --- /dev/null +++ b/vendor/plugins/acts_as_ferret/install.rb @@ -0,0 +1,19 @@ +# acts_as_ferret install script +require 'fileutils' + +def install(file) + puts "Installing: #{file}" + target = File.join(File.dirname(__FILE__), '..', '..', '..', file) + if File.exists?(target) + puts "target #{target} already exists, skipping" + else + FileUtils.cp File.join(File.dirname(__FILE__), file), target + end +end + +install File.join( 'script', 'ferret_start' ) +install File.join( 'script', 'ferret_stop' ) +install File.join( 'config', 'ferret_server.yml' ) + +puts IO.read(File.join(File.dirname(__FILE__), 'README')) + diff --git a/vendor/plugins/acts_as_ferret/lib/act_methods.rb b/vendor/plugins/acts_as_ferret/lib/act_methods.rb new file mode 100644 index 0000000..56c0f54 --- /dev/null +++ b/vendor/plugins/acts_as_ferret/lib/act_methods.rb @@ -0,0 +1,242 @@ +module ActsAsFerret #:nodoc: + + # This module defines the acts_as_ferret method and is included into + # ActiveRecord::Base + module ActMethods + + + def reloadable?; false end + + # declares a class as ferret-searchable. + # + # ====options: + # fields:: names all fields to include in the index. If not given, + # all attributes of the class will be indexed. You may also give + # symbols pointing to instance methods of your model here, i.e. + # to retrieve and index data from a related model. + # + # additional_fields:: names fields to include in the index, in addition + # to those derived from the db scheme. use if you want + # to add custom fields derived from methods to the db + # fields (which will be picked by aaf). This option will + # be ignored when the fields option is given, in that + # case additional fields get specified there. + # + # index_dir:: declares the directory where to put the index for this class. + # The default is RAILS_ROOT/index/RAILS_ENV/CLASSNAME. + # The index directory will be created if it doesn't exist. + # + # single_index:: set this to true to let this class use a Ferret + # index that is shared by all classes having :single_index set to true. + # :store_class_name is set to true implicitly, as well as index_dir, so + # don't bother setting these when using this option. the shared index + # will be located in index//shared . + # + # store_class_name:: to make search across multiple models (with either + # single_index or the multi_search method) useful, set + # this to true. the model class name will be stored in a keyword field + # named class_name + # + # reindex_batch_size:: reindexing is done in batches of this size, default is 1000 + # + # ferret:: Hash of Options that directly influence the way the Ferret engine works. You + # can use most of the options the Ferret::I class accepts here, too. Among the + # more useful are: + # + # or_default:: whether query terms are required by + # default (the default, false), or not (true) + # + # analyzer:: the analyzer to use for query parsing (default: nil, + # which means the ferret StandardAnalyzer gets used) + # + # default_field:: use to set one or more fields that are searched for query terms + # that don't have an explicit field list. This list should *not* + # contain any untokenized fields. If it does, you're asking + # for trouble (i.e. not getting results for queries having + # stop words in them). Aaf by default initializes the default field + # list to contain all tokenized fields. If you use :single_index => true, + # you really should set this option specifying your default field + # list (which should be equal in all your classes sharing the index). + # Otherwise you might get incorrect search results and you won't get + # any lazy loading of stored field data. + # + # For downwards compatibility reasons you can also specify the Ferret options in the + # last Hash argument. + def acts_as_ferret(options={}, ferret_options={}) + + # force local mode if running *inside* the Ferret server - somewhere the + # real indexing has to be done after all :-) + # Usually the automatic detection of server mode works fine, however if you + # require your model classes in environment.rb they will get loaded before the + # DRb server is started, so this code is executed too early and detection won't + # work. In this case you'll get endless loops resulting in "stack level too deep" + # errors. + # To get around this, start the server with the environment variable + # FERRET_USE_LOCAL_INDEX set to '1'. + 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" + options.delete(:remote) if ENV["FERRET_USE_LOCAL_INDEX"] || ActsAsFerret::Remote::Server.running + + if options[:remote] && options[:remote] !~ /^druby/ + # read server location from config/ferret_server.yml + options[:remote] = ActsAsFerret::Remote::Config.load("#{RAILS_ROOT}/config/ferret_server.yml")[:uri] rescue nil + end + + if options[:remote] + logger.debug "Will use remote index server which should be available at #{options[:remote]}" + else + logger.debug "Will use local index." + end + + + extend ClassMethods + extend SharedIndexClassMethods if options[:single_index] + + include InstanceMethods + include MoreLikeThis::InstanceMethods + + # AR hooks + after_create :ferret_create + after_update :ferret_update + after_destroy :ferret_destroy + + cattr_accessor :aaf_configuration + + # default config + self.aaf_configuration = { + :index_dir => "#{ActsAsFerret::index_dir}/#{self.name.underscore}", + :store_class_name => false, + :name => self.table_name, + :class_name => self.name, + :single_index => false, + :reindex_batch_size => 1000, + :ferret => {}, # Ferret config Hash + :ferret_fields => {} # list of indexed fields that will be filled later + } + + # merge aaf options with args + aaf_configuration.update(options) if options.is_a?(Hash) + # apply appropriate settings for shared index + if aaf_configuration[:single_index] + aaf_configuration[:index_dir] = "#{ActsAsFerret::index_dir}/shared" + aaf_configuration[:store_class_name] = true + end + + # set ferret default options + aaf_configuration[:ferret].reverse_merge!( :or_default => false, + :handle_parse_errors => true, + :default_field => nil # will be set later on + #:max_clauses => 512, + #:analyzer => Ferret::Analysis::StandardAnalyzer.new, + # :wild_card_downcase => true + ) + + # merge ferret options with those from second parameter hash + aaf_configuration[:ferret].update(ferret_options) if ferret_options.is_a?(Hash) + + unless options[:remote] + ActsAsFerret::ensure_directory aaf_configuration[:index_dir] + aaf_configuration[:index_base_dir] = aaf_configuration[:index_dir] + aaf_configuration[:index_dir] = find_last_index_version(aaf_configuration[:index_dir]) + logger.debug "using index in #{aaf_configuration[:index_dir]}" + end + + # these properties are somewhat vital to the plugin and shouldn't + # be overwritten by the user: + aaf_configuration[:ferret].update( + :key => (aaf_configuration[:single_index] ? [:id, :class_name] : :id), + :path => aaf_configuration[:index_dir], + :auto_flush => true, # slower but more secure in terms of locking problems TODO disable when running in drb mode? + :create_if_missing => true + ) + + if aaf_configuration[:fields] + add_fields(aaf_configuration[:fields]) + else + add_fields(self.new.attributes.keys.map { |k| k.to_sym }) + add_fields(aaf_configuration[:additional_fields]) + end + + # now that all fields have been added, we can initialize the default + # field list to be used by the query parser. + # It will include all content fields *not* marked as :untokenized. + # This fixes the otherwise failing CommentTest#test_stopwords. Basically + # this means that by default only tokenized fields (which is the default) + # will be searched. If you want to search inside the contents of an + # untokenized field, you'll have to explicitly specify it in your query. + # + # Unfortunately this is not very useful with a shared index (see + # http://projects.jkraemer.net/acts_as_ferret/ticket/85) + # You should consider specifying the default field list to search for as + # part of the ferret_options hash in your call to acts_as_ferret. + aaf_configuration[:ferret][:default_field] ||= if aaf_configuration[:single_index] + logger.warn "You really should set the acts_as_ferret :default_field option when using a shared index!" + '*' + else + aaf_configuration[:ferret_fields].keys.select do |f| + aaf_configuration[:ferret_fields][f][:index] != :untokenized + end + end + logger.info "default field list: #{aaf_configuration[:ferret][:default_field].inspect}" + + if options[:remote] + aaf_index.ensure_index_exists + end + end + + + protected + + # find the most recent version of an index + def find_last_index_version(basedir) + # check for versioned index + versions = Dir.entries(basedir).select do |f| + dir = File.join(basedir, f) + File.directory?(dir) && File.file?(File.join(dir, 'segments')) && f =~ /^\d+(_\d+)?$/ + end + if versions.any? + # select latest version + versions.sort! + File.join basedir, versions.last + else + basedir + end + end + + + # helper that defines a method that adds the given field to a ferret + # document instance + def define_to_field_method(field, options = {}) + options.reverse_merge!( :store => :no, + :highlight => :yes, + :index => :yes, + :term_vector => :with_positions_offsets, + :boost => 1.0 ) + options[:term_vector] = :no if options[:index] == :no + aaf_configuration[:ferret_fields][field] = options + define_method("#{field}_to_ferret".to_sym) do + begin + val = content_for_field_name(field) + rescue + logger.warn("Error retrieving value for field #{field}: #{$!}") + val = '' + end + logger.debug("Adding field #{field} with value '#{val}' to index") + val + end + end + + def add_fields(field_config) + if field_config.is_a? Hash + field_config.each_pair do |key,val| + define_to_field_method(key,val) + end + elsif field_config.respond_to?(:each) + field_config.each do |field| + define_to_field_method(field) + end + end + end + + end + +end diff --git a/vendor/plugins/acts_as_ferret/lib/acts_as_ferret.rb b/vendor/plugins/acts_as_ferret/lib/acts_as_ferret.rb new file mode 100644 index 0000000..47c8a99 --- /dev/null +++ b/vendor/plugins/acts_as_ferret/lib/acts_as_ferret.rb @@ -0,0 +1,160 @@ +# Copyright (c) 2006 Kasper Weibel Nielsen-Refs, Thomas Lockney, Jens Krämer +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +require 'active_support' +require 'active_record' +require 'set' +require 'ferret' + +require 'ferret_extensions' +require 'act_methods' +require 'class_methods' +require 'shared_index_class_methods' +require 'ferret_result' +require 'instance_methods' + +require 'multi_index' +require 'more_like_this' + +require 'index' +require 'local_index' +require 'shared_index' +require 'remote_index' + +require 'ferret_server' + + +# The Rails ActiveRecord Ferret Mixin. +# +# This mixin adds full text search capabilities to any Rails model. +# +# The current version emerged from on the original acts_as_ferret plugin done by +# Kasper Weibel and a modified version done by Thomas Lockney, which both can be +# found on the Ferret Wiki: http://ferret.davebalmain.com/trac/wiki/FerretOnRails. +# +# basic usage: +# include the following in your model class (specifiying the fields you want to get indexed): +# acts_as_ferret :fields => [ :title, :description ] +# +# now you can use ModelClass.find_by_contents(query) to find instances of your model +# whose indexed fields match a given query. All query terms are required by default, but +# explicit OR queries are possible. This differs from the ferret default, but imho is the more +# often needed/expected behaviour (more query terms result in less results). +# +# Released under the MIT license. +# +# Authors: +# Kasper Weibel Nielsen-Refs (original author) +# Jens Kraemer (active maintainer) +# +module ActsAsFerret + + # global Hash containing all multi indexes created by all classes using the plugin + # key is the concatenation of alphabetically sorted names of the classes the + # searcher searches. + @@multi_indexes = Hash.new + def self.multi_indexes; @@multi_indexes end + + # global Hash containing the ferret indexes of all classes using the plugin + # key is the index directory. + @@ferret_indexes = Hash.new + def self.ferret_indexes; @@ferret_indexes end + + + # decorator that adds a total_hits accessor to search result arrays + class SearchResults + attr_reader :total_hits + def initialize(results, total_hits) + @results = results + @total_hits = total_hits + end + def method_missing(symbol, *args, &block) + @results.send(symbol, *args, &block) + end + def respond_to?(name) + self.methods.include?(name) || @results.respond_to?(name) + end + end + + def self.ensure_directory(dir) + FileUtils.mkdir_p dir unless (File.directory?(dir) || File.symlink?(dir)) + end + + # make sure the default index base dir exists. by default, all indexes are created + # under RAILS_ROOT/index/RAILS_ENV + def self.init_index_basedir + index_base = "#{RAILS_ROOT}/index" + @@index_dir = "#{index_base}/#{RAILS_ENV}" + end + + mattr_accessor :index_dir + init_index_basedir + + def self.append_features(base) + super + base.extend(ClassMethods) + end + + # builds a FieldInfos instance for creation of an index containing fields + # for the given model classes. + def self.field_infos(models) + # default attributes for fields + fi = Ferret::Index::FieldInfos.new(:store => :no, + :index => :yes, + :term_vector => :no, + :boost => 1.0) + # primary key + fi.add_field(:id, :store => :yes, :index => :untokenized) + fields = {} + have_class_name = false + models.each do |model| + fields.update(model.aaf_configuration[:ferret_fields]) + # class_name + if !have_class_name && model.aaf_configuration[:store_class_name] + fi.add_field(:class_name, :store => :yes, :index => :untokenized) + have_class_name = true + end + end + fields.each_pair do |field, options| + fi.add_field(field, { :store => :no, + :index => :yes }.update(options)) + end + return fi + end + + def self.close_multi_indexes + # close combined index readers, just in case + # this seems to fix a strange test failure that seems to relate to a + # multi_index looking at an old version of the content_base index. + multi_indexes.each_pair do |key, index| + # puts "#{key} -- #{self.name}" + # TODO only close those where necessary (watch inheritance, where + # self.name is base class of a class where key is made from) + index.close #if key =~ /#{self.name}/ + end + multi_indexes.clear + end + +end + +# include acts_as_ferret method into ActiveRecord::Base +ActiveRecord::Base.extend ActsAsFerret::ActMethods + + diff --git a/vendor/plugins/acts_as_ferret/lib/class_methods.rb b/vendor/plugins/acts_as_ferret/lib/class_methods.rb new file mode 100644 index 0000000..d0b162c --- /dev/null +++ b/vendor/plugins/acts_as_ferret/lib/class_methods.rb @@ -0,0 +1,316 @@ +module ActsAsFerret + + module ClassMethods + + # rebuild the index from all data stored for this model. + # This is called automatically when no index exists yet. + # + # When calling this method manually, you can give any additional + # model classes that should also go into this index as parameters. + # Useful when using the :single_index option. + # Note that attributes named the same in different models will share + # the same field options in the shared index. + def rebuild_index(*models) + models << self unless models.include?(self) + aaf_index.rebuild_index models.map(&:to_s) + index_dir = find_last_index_version(aaf_configuration[:index_base_dir]) unless aaf_configuration[:remote] + end + + # runs across all records yielding those to be indexed when the index is rebuilt + def records_for_rebuild(batch_size = 1000) + transaction do + if connection.class.name =~ /Mysql/ && primary_key == 'id' + logger.info "using mysql specific batched find :all" + offset = 0 + while (rows = find :all, :conditions => ["id > ?", offset ], :limit => batch_size).any? + offset = rows.last.id + yield rows, offset + end + else + # sql server adapter won't batch correctly without defined ordering + order = "#{primary_key} ASC" if connection.class.name =~ /SQLServer/ + 0.step(self.count, batch_size) do |offset| + yield find( :all, :limit => batch_size, :offset => offset, :order => order ), offset + end + end + end + end + + # Switches this class to a new index located in dir. + # Used by the DRb server when switching to a new index version. + def index_dir=(dir) + logger.debug "changing index dir to #{dir}" + aaf_configuration[:index_dir] = aaf_configuration[:ferret][:path] = dir + aaf_index.reopen! + logger.debug "index dir is now #{dir}" + end + + # Retrieve the index instance for this model class. This can either be a + # LocalIndex, or a RemoteIndex instance. + # + # Index instances are stored in a hash, using the index directory + # as the key. So model classes sharing a single index will share their + # Index object, too. + def aaf_index + ActsAsFerret::ferret_indexes[aaf_configuration[:index_dir]] ||= create_index_instance + end + + # Finds instances by searching the Ferret index. Terms are ANDed by default, use + # OR between terms for ORed queries. Or specify +:or_default => true+ in the + # +:ferret+ options hash of acts_as_ferret. + # + # == options: + # offset:: first hit to retrieve (useful for paging) + # limit:: number of hits to retrieve, or :all to retrieve + # all results + # lazy:: Array of field names whose contents should be read directly + # from the index. Those fields have to be marked + # +:store => :yes+ in their field options. Give true to get all + # stored fields. Note that if you have a shared index, you have + # to explicitly state the fields you want to fetch, true won't + # work here) + # models:: only for single_index scenarios: an Array of other Model classes to + # include in this search. Use :all to query all models. + # + # +find_options+ is a hash passed on to active_record's find when + # retrieving the data from db, useful to i.e. prefetch relationships with + # :include or to specify additional filter criteria with :conditions. + # + # This method returns a +SearchResults+ instance, which really is an Array that has + # been decorated with a total_hits attribute holding the total number of hits. + # + # Please keep in mind that the number of total hits might be wrong if you specify + # both ferret options and active record find_options that somehow limit the result + # set (e.g. +:num_docs+ and some +:conditions+). + def find_with_ferret(q, options = {}, find_options = {}) + total_hits, result = find_records_lazy_or_not q, options, find_options + logger.debug "Query: #{q}\ntotal hits: #{total_hits}, results delivered: #{result.size}" + return SearchResults.new(result, total_hits) + end + alias find_by_contents find_with_ferret + + + + # Returns the total number of hits for the given query + # To count the results of a multi_search query, specify an array of + # class names with the :models option. + def total_hits(q, options={}) + if models = options[:models] + options[:models] = add_self_to_model_list_if_necessary(models).map(&:to_s) + end + aaf_index.total_hits(q, options) + end + + # Finds instance model name, ids and scores by contents. + # Useful e.g. if you want to search across models or do not want to fetch + # all result records (yet). + # + # Options are the same as for find_by_contents + # + # A block can be given too, it will be executed with every result: + # find_id_by_contents(q, options) do |model, id, score| + # id_array << id + # scores_by_id[id] = score + # end + # NOTE: in case a block is given, only the total_hits value will be returned + # instead of the [total_hits, results] array! + # + def find_id_by_contents(q, options = {}, &block) + deprecated_options_support(options) + aaf_index.find_id_by_contents(q, options, &block) + end + + # requires the store_class_name option of acts_as_ferret to be true + # for all models queried this way. + def multi_search(query, additional_models = [], options = {}, find_options = {}) + result = [] + + if options[:lazy] + logger.warn "find_options #{find_options} are ignored because :lazy => true" unless find_options.empty? + total_hits = id_multi_search(query, additional_models, options) do |model, id, score, data| + result << FerretResult.new(model, id, score, data) + end + else + id_arrays = {} + rank = 0 + total_hits = id_multi_search(query, additional_models, options) do |model, id, score, data| + id_arrays[model] ||= {} + id_arrays[model][id] = [ rank += 1, score ] + end + result = retrieve_records(id_arrays, find_options) + end + + SearchResults.new(result, total_hits) + end + + # returns an array of hashes, each containing :class_name, + # :id and :score for a hit. + # + # if a block is given, class_name, id and score of each hit will + # be yielded, and the total number of hits is returned. + def id_multi_search(query, additional_models = [], options = {}, &proc) + deprecated_options_support(options) + additional_models = add_self_to_model_list_if_necessary(additional_models) + aaf_index.id_multi_search(query, additional_models.map(&:to_s), options, &proc) + end + + + protected + + def add_self_to_model_list_if_necessary(models) + models = [ models ] unless models.is_a? Array + models << self unless models.include?(self) + end + + def find_records_lazy_or_not(q, options = {}, find_options = {}) + if options[:lazy] + logger.warn "find_options #{find_options} are ignored because :lazy => true" unless find_options.empty? + lazy_find_by_contents q, options + else + ar_find_by_contents q, options, find_options + end + end + + def ar_find_by_contents(q, options = {}, find_options = {}) + result_ids = {} + total_hits = find_id_by_contents(q, options) do |model, id, score, data| + # stores ids, index of each id for later ordering of + # results, and score + result_ids[id] = [ result_ids.size + 1, score ] + end + + result = retrieve_records( { self.name => result_ids }, find_options ) + + if find_options[:conditions] + if options[:limit] != :all + # correct result size if the user specified conditions + # wenn conditions: options[:limit] != :all --> ferret-query mit :all wiederholen und select count machen + result_ids = {} + find_id_by_contents(q, options.update(:limit => :all)) do |model, id, score, data| + result_ids[id] = [ result_ids.size + 1, score ] + end + total_hits = count_records( { self.name => result_ids }, find_options ) + else + total_hits = result.length + end + end + + [ total_hits, result ] + end + + def lazy_find_by_contents(q, options = {}) + result = [] + total_hits = find_id_by_contents(q, options) do |model, id, score, data| + result << FerretResult.new(model, id, score, data) + end + [ total_hits, result ] + end + + + def model_find(model, id, find_options = {}) + model.constantize.find(id, find_options) + end + + # retrieves search result records from a data structure like this: + # { 'Model1' => { '1' => [ rank, score ], '2' => [ rank, score ] } + # + # TODO: in case of STI AR will filter out hits from other + # classes for us, but this + # will lead to less results retrieved --> scoping of ferret query + # to self.class is still needed. + # from the ferret ML (thanks Curtis Hatter) + # > I created a method in my base STI class so I can scope my query. For scoping + # > I used something like the following line: + # > + # > query << " role:#{self.class.eql?(Contents) '*' : self.class}" + # > + # > Though you could make it more generic by simply asking + # > "self.descends_from_active_record?" which is how rails decides if it should + # > scope your "find" query for STI models. You can check out "base.rb" in + # > activerecord to see that. + # but maybe better do the scoping in find_id_by_contents... + def retrieve_records(id_arrays, find_options = {}) + result = [] + # get objects for each model + id_arrays.each do |model, id_array| + next if id_array.empty? + begin + model = model.constantize + rescue + raise "Please use ':store_class_name => true' if you want to use multi_search.\n#{$!}" + end + + # check for include association that might only exist on some models in case of multi_search + filtered_include_options = [] + if include_options = find_options[:include] + include_options.each do |include_option| + 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) + end + end + filtered_include_options=nil if filtered_include_options.empty? + + # fetch + tmp_result = nil + model.send(:with_scope, :find => find_options) do + tmp_result = model.find( :all, :conditions => [ + "#{model.table_name}.#{model.primary_key} in (?)", id_array.keys ], + :include => filtered_include_options ) + end + + # set scores and rank + tmp_result.each do |record| + record.ferret_rank, record.ferret_score = id_array[record.id.to_s] + end + # merge with result array + result.concat tmp_result + end + + # order results as they were found by ferret, unless an AR :order + # option was given + result.sort! { |a, b| a.ferret_rank <=> b.ferret_rank } unless find_options[:order] + return result + end + + def count_records(id_arrays, find_options = {}) + count = 0 + id_arrays.each do |model, id_array| + next if id_array.empty? + begin + model = model.constantize + model.send(:with_scope, :find => find_options) do + count += model.count(:conditions => [ "#{model.table_name}.#{model.primary_key} in (?)", + id_array.keys ]) + end + rescue TypeError + raise "#{model} must use :store_class_name option if you want to use multi_search against it.\n#{$!}" + end + end + count + end + + def deprecated_options_support(options) + if options[:num_docs] + logger.warn ":num_docs is deprecated, use :limit instead!" + options[:limit] ||= options[:num_docs] + end + if options[:first_doc] + logger.warn ":first_doc is deprecated, use :offset instead!" + options[:offset] ||= options[:first_doc] + end + end + + # creates a new Index instance. + def create_index_instance + if aaf_configuration[:remote] + RemoteIndex + elsif aaf_configuration[:single_index] + SharedIndex + else + LocalIndex + end.new(aaf_configuration) + end + + end + +end + diff --git a/vendor/plugins/acts_as_ferret/lib/ferret_cap_tasks.rb b/vendor/plugins/acts_as_ferret/lib/ferret_cap_tasks.rb new file mode 100644 index 0000000..569c169 --- /dev/null +++ b/vendor/plugins/acts_as_ferret/lib/ferret_cap_tasks.rb @@ -0,0 +1,21 @@ +# Ferret DRb server Capistrano tasks +# Usage: +# Add require 'vendor/plugins/acts_as_ferret/lib/ferret_cap_tasks' to your +# config/deploy.rb +# call ferret.restart where you restart your Mongrels. +# ferret.stop and ferret.start are available, too. +module FerretCapTasks + def start + run "cd #{current_path}; RAILS_ENV=production script/ferret_start" + end + + def stop + run "cd #{current_path}; RAILS_ENV=production script/ferret_stop" + end + + def restart + stop + start + end +end +Capistrano.plugin :ferret, FerretCapTasks diff --git a/vendor/plugins/acts_as_ferret/lib/ferret_extensions.rb b/vendor/plugins/acts_as_ferret/lib/ferret_extensions.rb new file mode 100644 index 0000000..efffe1d --- /dev/null +++ b/vendor/plugins/acts_as_ferret/lib/ferret_extensions.rb @@ -0,0 +1,81 @@ +module Ferret + + + class Index::Index + attr_accessor :batch_size + attr_accessor :logger + + def index_models(models) + models.each { |model| index_model model } + flush + optimize + close + ActsAsFerret::close_multi_indexes + end + + def index_model(model) + @batch_size ||= 0 + work_done = 0 + batch_time = 0 + logger.info "reindexing model #{model.name}" + + model_count = model.count.to_f + model.records_for_rebuild(@batch_size) do |records, offset| + #records = [ records ] unless records.is_a?(Array) + batch_time = measure_time { + records.each { |rec| self << rec.to_doc if rec.ferret_enabled?(true) } + }.to_f + work_done = offset.to_f / model_count * 100.0 if model_count > 0 + remaining_time = ( batch_time / @batch_size ) * ( model_count - offset + @batch_size ) + logger.info "reindex model #{model.name} : #{'%.2f' % work_done}% complete : #{'%.2f' % remaining_time} secs to finish" + end + end + + def measure_time + t1 = Time.now + yield + Time.now - t1 + end + + end + + + # add marshalling support to SortFields + class Search::SortField + def _dump(depth) + to_s + end + + def self._load(string) + case string + when /!/ : Ferret::Search::SortField::DOC_ID_REV + when // : Ferret::Search::SortField::DOC_ID + when '!' : Ferret::Search::SortField::SCORE_REV + when '' : Ferret::Search::SortField::SCORE + when /^(\w+):<(\w+)>(!)?$/ : new($1.to_sym, :type => $2.to_sym, :reverse => !$3.nil?) + else raise "invalid value: #{string}" + end + end + end + + # add marshalling support to Sort + class Search::Sort + def _dump(depth) + to_s + end + + def self._load(string) + # we exclude the last sorting as it is appended by new anyway + if string =~ /^Sort\[(.*?)((!)?)?\]$/ + sort_fields = $1.split(',').map do |value| + value.strip! + Ferret::Search::SortField._load value unless value.blank? + end + new sort_fields.compact + else + raise "invalid value: #{string}" + end + end + end + +end diff --git a/vendor/plugins/acts_as_ferret/lib/ferret_result.rb b/vendor/plugins/acts_as_ferret/lib/ferret_result.rb new file mode 100644 index 0000000..457926c --- /dev/null +++ b/vendor/plugins/acts_as_ferret/lib/ferret_result.rb @@ -0,0 +1,36 @@ +module ActsAsFerret + + # mixed into the FerretResult and AR classes calling acts_as_ferret + module ResultAttributes + # holds the score this record had when it was found via + # acts_as_ferret + attr_accessor :ferret_score + + attr_accessor :ferret_rank + end + + class FerretResult + include ResultAttributes + attr_accessor :id + + def initialize(model, id, score, data = {}) + @model = model.constantize + @id = id + @ferret_score = score + @data = data + end + + def method_missing(method, *args) + if @ar_record || @data[method].nil? + ferret_load_record unless @ar_record + @ar_record.send method, *args + else + @data[method] + end + end + + def ferret_load_record + @ar_record = @model.find(id) + end + end +end diff --git a/vendor/plugins/acts_as_ferret/lib/ferret_server.rb b/vendor/plugins/acts_as_ferret/lib/ferret_server.rb new file mode 100644 index 0000000..b5673f8 --- /dev/null +++ b/vendor/plugins/acts_as_ferret/lib/ferret_server.rb @@ -0,0 +1,131 @@ +require 'drb' +require 'thread' +require 'yaml' +require 'erb' + + +module ActsAsFerret + + module Remote + + module Config + class << self + DEFAULTS = { + 'host' => 'localhost', + 'port' => '9009' + } + # read connection settings from config file + def load(file = "#{RAILS_ROOT}/config/ferret_server.yml") + config = DEFAULTS.merge(YAML.load(ERB.new(IO.read(file)).result)) + if config = config[RAILS_ENV] + config[:uri] = "druby://#{config['host']}:#{config['port']}" + return config + end + {} + end + end + end + + # This class acts as a drb server listening for indexing and + # search requests from models declared to 'acts_as_ferret :remote => true' + # + # Usage: + # - modify RAILS_ROOT/config/ferret_server.yml to suit your needs. + # - environments for which no section in the config file exists will use + # the index locally (good for unit tests/development mode) + # - run script/ferret_start to start the server: + # RAILS_ENV=production script/ferret_start + # + class Server + + cattr_accessor :running + + def self.start(uri = nil) + ActiveRecord::Base.allow_concurrency = true + ActiveRecord::Base.logger = Logger.new("#{RAILS_ROOT}/log/ferret_server.log") + uri ||= ActsAsFerret::Remote::Config.load[:uri] + DRb.start_service(uri, ActsAsFerret::Remote::Server.new) + self.running = true + end + + def initialize + @logger = ActiveRecord::Base.logger + end + + # handles all incoming method calls, and sends them on to the LocalIndex + # instance of the correct model class. + # + # Calls are not queued atm, so this will block until the call returned. + # + def method_missing(name, *args) + @logger.debug "\#method_missing(#{name.inspect}, #{args.inspect})" + with_class args.shift do |clazz| + begin + clazz.aaf_index.send name, *args + rescue NoMethodError + @logger.debug "no luck, trying to call class method instead" + clazz.send name, *args + end + end + rescue + @logger.error "ferret server error #{$!}\n#{$!.backtrace.join '\n'}" + raise + end + + # make sure we have a versioned index in place, building one if necessary + def ensure_index_exists(class_name) + @logger.debug "DRb server: ensure_index_exists for class #{class_name}" + with_class class_name do |clazz| + dir = clazz.aaf_configuration[:index_dir] + unless File.directory?(dir) && File.file?(File.join(dir, 'segments')) && dir =~ %r{/\d+(_\d+)?$} + rebuild_index(clazz) + end + end + end + + # hides LocalIndex#rebuild_index to implement index versioning + def rebuild_index(clazz, *models) + with_class clazz do |clazz| + models = models.flatten.uniq.map(&:constantize) + models << clazz unless models.include?(clazz) + index = new_index_for(clazz, models) + @logger.debug "DRb server: rebuild index for class(es) #{models.inspect} in #{index.options[:path]}" + index.index_models models + new_version = File.join clazz.aaf_configuration[:index_base_dir], Time.now.utc.strftime('%Y%m%d%H%M%S') + # create a unique directory name (needed for unit tests where + # multiple rebuilds per second may occur) + if File.exists?(new_version) + i = 0 + i+=1 while File.exists?("#{new_version}_#{i}") + new_version << "_#{i}" + end + + File.rename index.options[:path], new_version + clazz.index_dir = new_version + end + end + + + protected + + def with_class(clazz, *args) + clazz = clazz.constantize if String === clazz + yield clazz, *args + end + + def new_index_for(clazz, models) + aaf_configuration = clazz.aaf_configuration + ferret_cfg = aaf_configuration[:ferret].dup + ferret_cfg.update :auto_flush => false, + :create => true, + :field_infos => ActsAsFerret::field_infos(models), + :path => File.join(aaf_configuration[:index_base_dir], 'rebuild') + returning Ferret::Index::Index.new ferret_cfg do |i| + i.batch_size = aaf_configuration[:reindex_batch_size] + i.logger = @logger + end + end + + end + end +end diff --git a/vendor/plugins/acts_as_ferret/lib/index.rb b/vendor/plugins/acts_as_ferret/lib/index.rb new file mode 100644 index 0000000..26e39ad --- /dev/null +++ b/vendor/plugins/acts_as_ferret/lib/index.rb @@ -0,0 +1,31 @@ +module ActsAsFerret + + # base class for local and remote indexes + class AbstractIndex + + attr_reader :aaf_configuration + attr_accessor :logger + def initialize(aaf_configuration) + @aaf_configuration = aaf_configuration + @logger = Logger.new("#{RAILS_ROOT}/log/ferret_index.log") + end + + class << self + def proxy_method(name, *args) + define_method name do |*args| + @server.send name, model_class_name, *args + end + end + + def index_proxy_method(*names) + names.each do |name| + define_method name do |*args| + @server.send :"index_#{name}", model_class_name, *args + end + end + end + + end + end + +end diff --git a/vendor/plugins/acts_as_ferret/lib/instance_methods.rb b/vendor/plugins/acts_as_ferret/lib/instance_methods.rb new file mode 100644 index 0000000..f83ab28 --- /dev/null +++ b/vendor/plugins/acts_as_ferret/lib/instance_methods.rb @@ -0,0 +1,126 @@ +module ActsAsFerret #:nodoc: + + module InstanceMethods + include ResultAttributes + + # Returns an array of strings with the matches highlighted. The +query+ can + # either be a String or a Ferret::Search::Query object. + # + # === Options + # + # field:: field to take the content from. This field has + # to have it's content stored in the index + # (:store => :yes in your call to aaf). If not + # given, all stored fields are searched, and the + # highlighted content found in all of them is returned. + # set :highlight => :no in the field options to + # avoid highlighting of contents from a :stored field. + # excerpt_length:: Default: 150. Length of excerpt to show. Highlighted + # terms will be in the centre of the excerpt. + # num_excerpts:: Default: 2. Number of excerpts to return. + # pre_tag:: Default: "". Tag to place to the left of the + # match. + # post_tag:: Default: "". This tag should close the + # +:pre_tag+. + # ellipsis:: Default: "...". This is the string that is appended + # at the beginning and end of excerpts (unless the + # excerpt hits the start or end of the field. You'll + # probably want to change this so a Unicode elipsis + # character. + def highlight(query, options = {}) + self.class.aaf_index.highlight(id, self.class.name, query, options) + end + + # re-eneable ferret indexing after a call to #disable_ferret + def ferret_enable; @ferret_disabled = nil end + + # returns true if ferret indexing is enabled + # the optional parameter will be true if the method is called by rebuild_index, + # and false otherwise. I.e. useful to enable a model only for indexing during + # scheduled reindex runs. + def ferret_enabled?(is_rebuild = false); @ferret_disabled.nil? end + + # Disable Ferret for a specified amount of time. ::once will disable + # Ferret for the next call to #save (this is the default), ::always will + # do so for all subsequent calls. + # To manually trigger reindexing of a record, you can call #ferret_update + # directly. + # + # When given a block, this will be executed without any ferret indexing of + # this object taking place. The optional argument in this case can be used + # to indicate if the object should be indexed after executing the block + # (::index_when_finished). Automatic Ferret indexing of this object will be + # turned on after the block has been executed. If passed ::index_when_true, + # the index will only be updated if the block evaluated not to false or nil. + def disable_ferret(option = :once) + if block_given? + @ferret_disabled = :always + result = yield + ferret_enable + ferret_update if option == :index_when_finished || (option == :index_when_true && result) + result + elsif [:once, :always].include?(option) + @ferret_disabled = option + else + raise ArgumentError.new("Invalid Argument #{option}") + end + end + + # add to index + def ferret_create + if ferret_enabled? + logger.debug "ferret_create/update: #{self.class.name} : #{self.id}" + self.class.aaf_index << self + else + ferret_enable if @ferret_disabled == :once + end + true # signal success to AR + end + alias :ferret_update :ferret_create + + + # remove from index + def ferret_destroy + logger.debug "ferret_destroy: #{self.class.name} : #{self.id}" + begin + self.class.aaf_index.remove self.id, self.class.name + rescue + logger.warn("Could not find indexed value for this object: #{$!}\n#{$!.backtrace}") + end + true # signal success to AR + end + + # turn this instance into a ferret document (which basically is a hash of + # fieldname => value pairs) + def to_doc + logger.debug "creating doc for class: #{self.class.name}, id: #{self.id}" + returning doc = Ferret::Document.new do + # store the id of each item + doc[:id] = self.id + + # store the class name if configured to do so + doc[:class_name] = self.class.name if aaf_configuration[:store_class_name] + + # iterate through the fields and add them to the document + aaf_configuration[:ferret_fields].each_pair do |field, config| + doc[field] = self.send("#{field}_to_ferret") unless config[:ignore] + end + end + end + + def document_number + self.class.aaf_index.document_number(id, self.class.name) + end + + def query_for_record + self.class.aaf_index.query_for_record(id, self.class.name) + end + + def content_for_field_name(field) + self[field] || self.instance_variable_get("@#{field.to_s}".to_sym) || self.send(field.to_sym) + end + + + end + +end diff --git a/vendor/plugins/acts_as_ferret/lib/local_index.rb b/vendor/plugins/acts_as_ferret/lib/local_index.rb new file mode 100644 index 0000000..d678ee2 --- /dev/null +++ b/vendor/plugins/acts_as_ferret/lib/local_index.rb @@ -0,0 +1,209 @@ +module ActsAsFerret + + class LocalIndex < AbstractIndex + include MoreLikeThis::IndexMethods + + + def initialize(aaf_configuration) + super + ensure_index_exists + end + + def reopen! + if @ferret_index + @ferret_index.close + @ferret_index = nil + end + logger.debug "reopening index at #{aaf_configuration[:ferret][:path]}" + ferret_index + end + + # The 'real' Ferret Index instance + def ferret_index + ensure_index_exists + returning @ferret_index ||= Ferret::Index::Index.new(aaf_configuration[:ferret]) do + @ferret_index.batch_size = aaf_configuration[:reindex_batch_size] + @ferret_index.logger = logger + end + end + + # Checks for the presence of a segments file in the index directory + # Rebuilds the index if none exists. + def ensure_index_exists + logger.debug "LocalIndex: ensure_index_exists at #{aaf_configuration[:index_dir]}" + unless File.file? "#{aaf_configuration[:index_dir]}/segments" + ActsAsFerret::ensure_directory(aaf_configuration[:index_dir]) + close + rebuild_index + end + end + + # Closes the underlying index instance + def close + @ferret_index.close if @ferret_index + rescue StandardError + # is raised when index already closed + ensure + @ferret_index = nil + end + + # rebuilds the index from all records of the model class this index belongs + # to. Arguments can be given in shared index scenarios to name multiple + # model classes to include in the index + def rebuild_index(*models) + models << aaf_configuration[:class_name] unless models.include?(aaf_configuration[:class_name]) + models = models.flatten.uniq.map(&:constantize) + logger.debug "rebuild index: #{models.inspect}" + index = Ferret::Index::Index.new(aaf_configuration[:ferret].dup.update(:auto_flush => false, + :field_infos => ActsAsFerret::field_infos(models), + :create => true)) + index.batch_size = aaf_configuration[:reindex_batch_size] + index.logger = logger + index.index_models models + end + + # Parses the given query string into a Ferret Query object. + def process_query(query) + # work around ferret bug in #process_query (doesn't ensure the + # reader is open) + ferret_index.synchronize do + ferret_index.send(:ensure_reader_open) + original_query = ferret_index.process_query(query) + end + end + + # Total number of hits for the given query. + # To count the results of a multi_search query, specify an array of + # class names with the :models option. + def total_hits(query, options = {}) + index = (models = options.delete(:models)) ? multi_index(models) : ferret_index + index.search(query, options).total_hits + end + + def determine_lazy_fields(options = {}) + stored_fields = options[:lazy] + if stored_fields && !(Array === stored_fields) + stored_fields = aaf_configuration[:ferret_fields].select { |field, config| config[:store] == :yes }.map(&:first) + end + logger.debug "stored_fields: #{stored_fields}" + return stored_fields + end + + # Queries the Ferret index to retrieve model class, id, score and the + # values of any fields stored in the index for each hit. + # If a block is given, these are yielded and the number of total hits is + # returned. Otherwise [total_hits, result_array] is returned. + def find_id_by_contents(query, options = {}) + result = [] + index = ferret_index + logger.debug "query: #{ferret_index.process_query query}" # TODO only enable this for debugging purposes + lazy_fields = determine_lazy_fields options + + total_hits = index.search_each(query, options) do |hit, score| + doc = index[hit] + model = aaf_configuration[:store_class_name] ? doc[:class_name] : aaf_configuration[:class_name] + # fetch stored fields if lazy loading + data = {} + lazy_fields.each { |field| data[field] = doc[field] } if lazy_fields + if block_given? + yield model, doc[:id], score, data + else + result << { :model => model, :id => doc[:id], :score => score, :data => data } + end + end + #logger.debug "id_score_model array: #{result.inspect}" + return block_given? ? total_hits : [total_hits, result] + end + + # Queries multiple Ferret indexes to retrieve model class, id and score for + # each hit. Use the models parameter to give the list of models to search. + # If a block is given, model, id and score are yielded and the number of + # total hits is returned. Otherwise [total_hits, result_array] is returned. + def id_multi_search(query, models, options = {}) + index = multi_index(models) + result = [] + lazy_fields = determine_lazy_fields options + total_hits = index.search_each(query, options) do |hit, score| + doc = index[hit] + # fetch stored fields if lazy loading + data = {} + lazy_fields.each { |field| data[field] = doc[field] } if lazy_fields + raise "':store_class_name => true' required for multi_search to work" if doc[:class_name].blank? + if block_given? + yield doc[:class_name], doc[:id], score, doc, data + else + result << { :model => doc[:class_name], :id => doc[:id], :score => score, :data => data } + end + end + return block_given? ? total_hits : [ total_hits, result ] + end + + ###################################### + # methods working on a single record + # called from instance_methods, here to simplify interfacing with the + # remote ferret server + # TODO having to pass id and class_name around like this isn't nice + ###################################### + + # add record to index + # record may be the full AR object, a Ferret document instance or a Hash + def add(record) + record = record.to_doc unless Hash === record || Ferret::Document === record + ferret_index << record + end + alias << add + + # delete record from index + def remove(id, class_name) + ferret_index.query_delete query_for_record(id, class_name) + end + + # highlight search terms for the record with the given id. + def highlight(id, class_name, query, options = {}) + options.reverse_merge! :num_excerpts => 2, :pre_tag => '', :post_tag => '' + highlights = [] + ferret_index.synchronize do + doc_num = document_number(id, class_name) + if options[:field] + highlights << ferret_index.highlight(query, doc_num, options) + else + query = process_query(query) # process only once + aaf_configuration[:ferret_fields].each_pair do |field, config| + next if config[:store] == :no || config[:highlight] == :no + options[:field] = field + highlights << ferret_index.highlight(query, doc_num, options) + end + end + end + return highlights.compact.flatten[0..options[:num_excerpts]-1] + end + + # retrieves the ferret document number of the record with the given id. + def document_number(id, class_name) + hits = ferret_index.search(query_for_record(id, class_name)) + return hits.hits.first.doc if hits.total_hits == 1 + raise "cannot determine document number from primary key: #{id}" + end + + # build a ferret query matching only the record with the given id + # the class name only needs to be given in case of a shared index configuration + def query_for_record(id, class_name = nil) + Ferret::Search::TermQuery.new(:id, id.to_s) + end + + + protected + + # returns a MultiIndex instance operating on a MultiReader + def multi_index(model_classes) + model_classes.map!(&:constantize) if String === model_classes.first + model_classes.sort! { |a, b| a.name <=> b.name } + key = model_classes.inject("") { |s, clazz| s + clazz.name } + multi_config = aaf_configuration[:ferret].dup + multi_config.delete :default_field # we don't want the default field list of *this* class for multi_searching + ActsAsFerret::multi_indexes[key] ||= MultiIndex.new(model_classes, multi_config) + end + + end + +end diff --git a/vendor/plugins/acts_as_ferret/lib/more_like_this.rb b/vendor/plugins/acts_as_ferret/lib/more_like_this.rb new file mode 100644 index 0000000..72356c5 --- /dev/null +++ b/vendor/plugins/acts_as_ferret/lib/more_like_this.rb @@ -0,0 +1,217 @@ +module ActsAsFerret #:nodoc: + + module MoreLikeThis + + module InstanceMethods + + # returns other instances of this class, which have similar contents + # like this one. Basically works like this: find out n most interesting + # (i.e. characteristic) terms from this document, and then build a + # query from those which is run against the whole index. Which terms + # are interesting is decided on variour criteria which can be + # influenced by the given options. + # + # The algorithm used here is a quite straight port of the MoreLikeThis class + # from Apache Lucene. + # + # options are: + # :field_names : Array of field names to use for similarity search (mandatory) + # :min_term_freq => 2, # Ignore terms with less than this frequency in the source doc. + # :min_doc_freq => 5, # Ignore words which do not occur in at least this many docs + # :min_word_length => nil, # Ignore words shorter than this length (longer words tend to + # be more characteristic for the document they occur in). + # :max_word_length => nil, # Ignore words if greater than this len. + # :max_query_terms => 25, # maximum number of terms in the query built + # :max_num_tokens => 5000, # maximum number of tokens to examine in a single field + # :boost => false, # when true, a boost according to the relative score of + # a term is applied to this Term's TermQuery. + # :similarity => 'DefaultAAFSimilarity' # the similarity implementation to use (the default + # equals Ferret's internal similarity implementation) + # :analyzer => 'Ferret::Analysis::StandardAnalyzer' # class name of the analyzer to use + # :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 + # ferret_options : Ferret options handed over to find_by_contents (i.e. for limits and sorting) + # ar_options : options handed over to find_by_contents for AR scoping + def more_like_this(options = {}, ferret_options = {}, ar_options = {}) + options = { + :field_names => nil, # Default field names + :min_term_freq => 2, # Ignore terms with less than this frequency in the source doc. + :min_doc_freq => 5, # Ignore words which do not occur in at least this many docs + :min_word_length => 0, # Ignore words if less than this len. Default is not to ignore any words. + :max_word_length => 0, # Ignore words if greater than this len. Default is not to ignore any words. + :max_query_terms => 25, # maximum number of terms in the query built + :max_num_tokens => 5000, # maximum number of tokens to analyze when analyzing contents + :boost => false, + :similarity => 'ActsAsFerret::MoreLikeThis::DefaultAAFSimilarity', # class name of the similarity implementation to use + :analyzer => 'Ferret::Analysis::StandardAnalyzer', # class name of the analyzer to use + :append_to_query => nil, + :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 + }.update(options) + #index.search_each('id:*') do |doc, score| + # puts "#{doc} == #{index[doc][:description]}" + #end + clazz = options[:base_class] + options[:base_class] = clazz.name + query = clazz.aaf_index.build_more_like_this_query(self.id, self.class.name, options) + options[:append_to_query].call(query) if options[:append_to_query] + clazz.find_by_contents(query, ferret_options, ar_options) + end + + end + + module IndexMethods + + # TODO to allow morelikethis for unsaved records, we have to give the + # unsaved record's data to this method. check how this will work out + # via drb... + def build_more_like_this_query(id, class_name, options) + [:similarity, :analyzer].each { |sym| options[sym] = options[sym].constantize.new } + ferret_index.synchronize do # avoid that concurrent writes close our reader + ferret_index.send(:ensure_reader_open) + reader = ferret_index.send(:reader) + term_freq_map = retrieve_terms(id, class_name, reader, options) + priority_queue = create_queue(term_freq_map, reader, options) + create_query(id, class_name, priority_queue, options) + end + end + + protected + + def create_query(id, class_name, priority_queue, options={}) + query = Ferret::Search::BooleanQuery.new + qterms = 0 + best_score = nil + while(cur = priority_queue.pop) + term_query = Ferret::Search::TermQuery.new(cur.field, cur.word) + + if options[:boost] + # boost term according to relative score + # TODO untested + best_score ||= cur.score + term_query.boost = cur.score / best_score + end + begin + query.add_query(term_query, :should) + rescue Ferret::Search::BooleanQuery::TooManyClauses + break + end + qterms += 1 + break if options[:max_query_terms] > 0 && qterms >= options[:max_query_terms] + end + # exclude the original record + query.add_query(query_for_record(id, class_name), :must_not) + return query + end + + + + # creates a term/term_frequency map for terms from the fields + # given in options[:field_names] + def retrieve_terms(id, class_name, reader, options) + raise "more_like_this atm only works on saved records" if id.nil? + document_number = document_number(id, class_name) rescue nil + field_names = options[:field_names] + max_num_tokens = options[:max_num_tokens] + term_freq_map = Hash.new(0) + doc = nil + record = nil + field_names.each do |field| + #puts "field: #{field}" + term_freq_vector = reader.term_vector(document_number, field) if document_number + #if false + if term_freq_vector + # use stored term vector + # puts 'using stored term vector' + term_freq_vector.terms.each do |term| + term_freq_map[term.text] += term.positions.size unless noise_word?(term.text, options) + end + else + # puts 'no stored term vector' + # no term vector stored, but we have stored the contents in the index + # -> extract terms from there + content = nil + if document_number + doc = reader[document_number] + content = doc[field] + end + unless content + # no term vector, no stored content, so try content from this instance + record ||= options[:base_class].constantize.find(id) + content = record.content_for_field_name(field.to_s) + end + puts "have doc: #{doc[:id]} with #{field} == #{content}" + token_count = 0 + + ts = options[:analyzer].token_stream(field, content) + while token = ts.next + break if (token_count+=1) > max_num_tokens + next if noise_word?(token.text, options) + term_freq_map[token.text] += 1 + end + end + end + term_freq_map + end + + # create an ordered(by score) list of word,fieldname,score + # structures + def create_queue(term_freq_map, reader, options) + pq = Array.new(term_freq_map.size) + + similarity = options[:similarity] + num_docs = reader.num_docs + term_freq_map.each_pair do |word, tf| + # filter out words that don't occur enough times in the source + next if options[:min_term_freq] && tf < options[:min_term_freq] + + # go through all the fields and find the largest document frequency + top_field = options[:field_names].first + doc_freq = 0 + options[:field_names].each do |field_name| + freq = reader.doc_freq(field_name, word) + if freq > doc_freq + top_field = field_name + doc_freq = freq + end + end + # filter out words that don't occur in enough docs + next if options[:min_doc_freq] && doc_freq < options[:min_doc_freq] + next if doc_freq == 0 # index update problem ? + + idf = similarity.idf(doc_freq, num_docs) + score = tf * idf + pq << FrequencyQueueItem.new(word, top_field, score) + end + pq.compact! + pq.sort! { |a,b| a.score<=>b.score } + return pq + end + + def noise_word?(text, options) + len = text.length + ( + (options[:min_word_length] > 0 && len < options[:min_word_length]) || + (options[:max_word_length] > 0 && len > options[:max_word_length]) || + (options[:stop_words] && options.include?(text)) + ) + end + + end + + class DefaultAAFSimilarity + def idf(doc_freq, num_docs) + return 0.0 if num_docs == 0 + return Math.log(num_docs.to_f/(doc_freq+1)) + 1.0 + end + end + + + class FrequencyQueueItem + attr_reader :word, :field, :score + def initialize(word, field, score) + @word = word; @field = field; @score = score + end + end + + end +end + diff --git a/vendor/plugins/acts_as_ferret/lib/multi_index.rb b/vendor/plugins/acts_as_ferret/lib/multi_index.rb new file mode 100644 index 0000000..adbfb7b --- /dev/null +++ b/vendor/plugins/acts_as_ferret/lib/multi_index.rb @@ -0,0 +1,83 @@ +module ActsAsFerret #:nodoc: + + # this class is not threadsafe + class MultiIndex + + def initialize(model_classes, options = {}) + @model_classes = model_classes + # ensure all models indexes exist + @model_classes.each { |m| m.aaf_index.ensure_index_exists } + default_fields = @model_classes.inject([]) do |fields, c| + fields + [ c.aaf_configuration[:ferret][:default_field] ].flatten + end + @options = { + :default_field => default_fields + }.update(options) + end + + def search(query, options={}) + #puts "querystring: #{query.to_s}" + query = process_query(query) + #puts "parsed query: #{query.to_s}" + searcher.search(query, options) + end + + def search_each(query, options = {}, &block) + query = process_query(query) + searcher.search_each(query, options, &block) + end + + # checks if all our sub-searchers still are up to date + def latest? + return false unless @reader + # segfaults with 0.10.4 --> TODO report as bug @reader.latest? + @sub_readers.each do |r| + return false unless r.latest? + end + true + end + + def searcher + ensure_searcher + @searcher + end + + def doc(i) + searcher[i] + end + alias :[] :doc + + def query_parser + @query_parser ||= Ferret::QueryParser.new(@options) + end + + def process_query(query) + query = query_parser.parse(query) if query.is_a?(String) + return query + end + + def close + @searcher.close if @searcher + @reader.close if @reader + end + + protected + + def ensure_searcher + unless latest? + @sub_readers = @model_classes.map { |clazz| + begin + reader = Ferret::Index::IndexReader.new(clazz.aaf_configuration[:index_dir]) + rescue Exception + raise "error opening #{clazz.aaf_configuration[:index_dir]}: #{$!}" + end + } + close + @reader = Ferret::Index::IndexReader.new(@sub_readers) + @searcher = Ferret::Search::Searcher.new(@reader) + end + end + + end # of class MultiIndex + +end diff --git a/vendor/plugins/acts_as_ferret/lib/remote_index.rb b/vendor/plugins/acts_as_ferret/lib/remote_index.rb new file mode 100644 index 0000000..30ae3a7 --- /dev/null +++ b/vendor/plugins/acts_as_ferret/lib/remote_index.rb @@ -0,0 +1,50 @@ +require 'drb' +module ActsAsFerret + + # This index implementation connects to a remote ferret server instance. It + # basically forwards all calls to the remote server. + class RemoteIndex < AbstractIndex + + def initialize(config) + @config = config + @ferret_config = config[:ferret] + @server = DRbObject.new(nil, config[:remote]) + end + + def method_missing(method_name, *args) + args.unshift model_class_name + @server.send(method_name, *args) + end + + def find_id_by_contents(q, options = {}, &proc) + total_hits, results = @server.find_id_by_contents(model_class_name, q, options) + block_given? ? yield_results(total_hits, results, &proc) : [ total_hits, results ] + end + + def id_multi_search(query, models, options, &proc) + total_hits, results = @server.id_multi_search(model_class_name, query, models, options) + block_given? ? yield_results(total_hits, results, &proc) : [ total_hits, results ] + end + + # add record to index + def add(record) + @server.add record.class.name, record.to_doc + end + alias << add + + private + + def yield_results(total_hits, results) + results.each do |result| + yield result[:model], result[:id], result[:score], result[:data] + end + total_hits + end + + def model_class_name + @config[:class_name] + end + + end + +end diff --git a/vendor/plugins/acts_as_ferret/lib/shared_index.rb b/vendor/plugins/acts_as_ferret/lib/shared_index.rb new file mode 100644 index 0000000..21cadfd --- /dev/null +++ b/vendor/plugins/acts_as_ferret/lib/shared_index.rb @@ -0,0 +1,14 @@ +module ActsAsFerret + + class SharedIndex < LocalIndex + + # build a ferret query matching only the record with the given id and class + def query_for_record(id, class_name) + returning bq = Ferret::Search::BooleanQuery.new do + bq.add_query(Ferret::Search::TermQuery.new(:id, id.to_s), :must) + bq.add_query(Ferret::Search::TermQuery.new(:class_name, class_name), :must) + end + end + + end +end diff --git a/vendor/plugins/acts_as_ferret/lib/shared_index_class_methods.rb b/vendor/plugins/acts_as_ferret/lib/shared_index_class_methods.rb new file mode 100644 index 0000000..047b945 --- /dev/null +++ b/vendor/plugins/acts_as_ferret/lib/shared_index_class_methods.rb @@ -0,0 +1,90 @@ +module ActsAsFerret + + # class methods for classes using acts_as_ferret :single_index => true + module SharedIndexClassMethods + + def find_id_by_contents(q, options = {}, &block) + # add class name scoping to query if necessary + unless options[:models] == :all # search needs to be restricted by one or more class names + options[:models] ||= [] + # add this class to the list of given models + options[:models] << self unless options[:models].include?(self) + # keep original query + original_query = q + + if original_query.is_a? String + model_query = options[:models].map(&:name).join '|' + q << %{ +class_name:"#{model_query}"} + else + q = Ferret::Search::BooleanQuery.new + q.add_query(original_query, :must) + model_query = Ferret::Search::BooleanQuery.new + options[:models].each do |model| + model_query.add_query(Ferret::Search::TermQuery.new(:class_name, model.name), :should) + end + q.add_query(model_query, :must) + end + end + options.delete :models + + super(q, options, &block) + end + + # Overrides the standard find_by_contents for searching a shared index. + # + # please note that records from different models will be fetched in + # separate sql calls, so any sql order_by clause given with + # find_options[:order] will be ignored. + def find_by_contents(q, options = {}, find_options = {}) + if order = find_options.delete(:order) + logger.warn "using a shared index, so ignoring order_by clause #{order}" + end + total_hits, result = find_records_lazy_or_not q, options, find_options + # sort so results have the same order they had when originally retrieved + # from ferret + return SearchResults.new(result, total_hits) + end + + protected + + def ar_find_by_contents(q, options = {}, find_options = {}) + total_hits, id_arrays = collect_results(q, options) + result = retrieve_records(id_arrays, find_options) + result.sort! { |a, b| id_arrays[a.class.name][a.id.to_s].first <=> id_arrays[b.class.name][b.id.to_s].first } + [ total_hits, result ] + end + + def collect_results(q, options = {}) + id_arrays = {} + # get object ids for index hits + rank = 0 + total_hits = find_id_by_contents(q, options) do |model, id, score, data| + id_arrays[model] ||= {} + # store result rank and score + id_arrays[model][id] = [ rank += 1, score ] + end + [ total_hits, id_arrays ] + end + + + # determine all field names in the shared index + # TODO unused +# def single_index_field_names(models) +# @single_index_field_names ||= ( +# searcher = Ferret::Search::Searcher.new(class_index_dir) +# if searcher.reader.respond_to?(:get_field_names) +# (searcher.reader.send(:get_field_names) - ['id', 'class_name']).to_a +# else +# puts <<-END +#unable to retrieve field names for class #{self.name}, please +#consider naming all indexed fields in your call to acts_as_ferret! +# END +# models.map { |m| m.content_columns.map { |col| col.name } }.flatten +# end +# ) +# +# end + + end +end + diff --git a/vendor/plugins/acts_as_ferret/rakefile b/vendor/plugins/acts_as_ferret/rakefile new file mode 100644 index 0000000..e614565 --- /dev/null +++ b/vendor/plugins/acts_as_ferret/rakefile @@ -0,0 +1,131 @@ +# rakefile for acts_as_ferret. +# use to create a gem or generate rdoc api documentation. +# +# RELEASE creation: +# rake release REL=x.y.z + +require 'rake' +require 'rake/rdoctask' +require 'rake/packagetask' +require 'rake/gempackagetask' +require 'rake/testtask' +require 'rake/contrib/rubyforgepublisher' + +def announce(msg='') + STDERR.puts msg +end + + +PKG_NAME = 'acts_as_ferret' +PKG_VERSION = ENV['REL'] +PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}" +RUBYFORGE_PROJECT = 'actsasferret' +RUBYFORGE_USER = 'jkraemer' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the acts_as_ferret plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the acts_as_ferret plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'html' + rdoc.title = "acts_as_ferret - Ferret based full text search for any ActiveRecord model" + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.options << '--main' << 'README' + rdoc.rdoc_files.include('README', 'LICENSE') + rdoc.template = "#{ENV['template']}.rb" if ENV['template'] + rdoc.rdoc_files.include('lib/**/*.rb') +end + +desc "Publish the API documentation" +task :pdoc => [:rdoc] do + Rake::RubyForgePublisher.new(RUBYFORGE_PROJECT, RUBYFORGE_USER).upload +end + +if PKG_VERSION + spec = Gem::Specification.new do |s| + s.name = PKG_NAME + s.version = PKG_VERSION + s.platform = Gem::Platform::RUBY + s.summary = "acts_as_ferret - Ferret based full text search for any ActiveRecord model" + s.files = Dir.glob('**/*', File::FNM_DOTMATCH).reject do |f| + [ /\.$/, /sqlite$/, /\.log$/, /^pkg/, /\.svn/, /\.\w+\.sw.$/, + /^html/, /\~$/, /\/\._/, /\/#/ ].any? {|regex| f =~ regex } + end + #s.files = FileList["{lib,test}/**/*"].to_a + %w(README MIT-LICENSE CHANGELOG) + # s.files.delete ... + s.require_path = 'lib' + s.autorequire = 'acts_as_ferret' + s.has_rdoc = true + # s.test_files = Dir['test/**/*_test.rb'] + s.author = "Jens Kraemer" + s.email = "jk@jkraemer.net" + s.homepage = "http://projects.jkraemer.net/acts_as_ferret" + end + + package_task = Rake::GemPackageTask.new(spec) do |pkg| + pkg.need_tar = true + end + + # Validate that everything is ready to go for a release. + task :prerelease do + announce + announce "**************************************************************" + announce "* Making RubyGem Release #{PKG_VERSION}" + announce "**************************************************************" + announce + # Are all source files checked in? + if ENV['RELTEST'] + announce "Release Task Testing, skipping checked-in file test" + else + announce "Pulling in svn..." + `svk pull .` + announce "Checking for unchecked-in files..." + data = `svk st` + unless data =~ /^$/ + fail "SVK status is not clean ... do you have unchecked-in files?" + end + announce "No outstanding checkins found ... OK" + announce "Pushing to svn..." + `svk push .` + end + end + + + desc "tag the new release" + task :tag => [ :prerelease ] do + reltag = "REL_#{PKG_VERSION.gsub(/\./, '_')}" + reltag << ENV['REUSE'].gsub(/\./, '_') if ENV['REUSE'] + announce "Tagging with [#{PKG_VERSION}]" + if ENV['RELTEST'] + announce "Release Task Testing, skipping tagging" + else + `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}` + `svn del -m 'remove old stable' svn://projects.jkraemer.net/acts_as_ferret/tags/stable` + `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` + end + end + + # Upload release to rubyforge + desc "Upload release to rubyforge" + task :prel => [ :tag, :prerelease, :package ] do + `rubyforge login` + release_command = "rubyforge add_release #{RUBYFORGE_PROJECT} #{PKG_NAME} '#{PKG_VERSION}' pkg/#{PKG_NAME}-#{PKG_VERSION}.gem" + puts release_command + system(release_command) + `rubyforge config #{RUBYFORGE_PROJECT}` + release_command = "rubyforge add_file #{RUBYFORGE_PROJECT} #{PKG_NAME} '#{PKG_VERSION}' pkg/#{PKG_NAME}-#{PKG_VERSION}.tgz" + puts release_command + system(release_command) + end + + desc 'Publish the gem and API docs' + task :release => [:pdoc, :prel ] + +end diff --git a/vendor/plugins/acts_as_ferret/script/ferret_server b/vendor/plugins/acts_as_ferret/script/ferret_server new file mode 100644 index 0000000..374a22e --- /dev/null +++ b/vendor/plugins/acts_as_ferret/script/ferret_server @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby + +# Ferret DRb server launcher script +# +# Place doc/ferret_server.yml into RAILS_ROOT/config and fit to taste. +# +# Start this script with script/runner and RAILS_ENV set. +# +# to run the unit tests against the drb server, start it with +# RAILS_ENV=test script/runner script/ferret_server +# and run your tests with the AAF_REMOTE environment variable set to a +# non-empty value + + +ActsAsFerret::Remote::Server.start +DRb.thread.join + + diff --git a/vendor/plugins/acts_as_ferret/script/ferret_start b/vendor/plugins/acts_as_ferret/script/ferret_start new file mode 100755 index 0000000..50e1909 --- /dev/null +++ b/vendor/plugins/acts_as_ferret/script/ferret_start @@ -0,0 +1,72 @@ +#!/usr/bin/env ruby +# Ferret DRb server launcher script +# +# Place doc/ferret_server.yml into RAILS_ROOT/config and fit to taste. Start +# it with RAILS_ENV set to the desired environment. +# +# +# To run the demo project's unit tests against the drb server, start it with +# +# RAILS_ENV=test script/ferret_start +# +# and run your tests with the AAF_REMOTE environment variable set to a +# non-empty value: +# +# AAF_REMOTE=true rake +# +# The server writes a log file in log/ferret_server.log, it's +# STDOUT gets redirected to log/ferret_server.out + +ENV['FERRET_USE_LOCAL_INDEX'] = 'true' +require File.dirname(__FILE__) + '/../config/boot' +require RAILS_ROOT + '/config/environment' + + +config = ActsAsFerret::Remote::Config.load +@pid_file = config['pid_file'] + +def write_pid_file + raise "No PID file defined" if @pid_file.blank? + open(@pid_file,"w") {|f| f.write(Process.pid) } +end + +def safefork + tryagain = true + + while tryagain + tryagain = false + begin + if pid = fork + return pid + end + rescue Errno::EWOULDBLOCK + sleep 5 + tryagain = true + end + end +end + +safefork and exit +at_exit do + File.unlink(@pid_file) if @pid_file && File.exists?(@pid_file) && File.read(@pid_file).to_i == Process.pid +end +print "Starting ferret DRb server..." +trap("TERM") { exit(0) } +sess_id = Process.setsid + + +begin + ActsAsFerret::Remote::Server.start + write_pid_file + puts "Done." + STDIN.reopen "/dev/null" # Free file descriptors and + STDOUT.reopen "#{RAILS_ROOT}/log/ferret_server.out", "a" # point them somewhere sensible + STDERR.reopen STDOUT # STDOUT/STDERR should go to a logfile +rescue + $stderr.puts "Error starting ferret DRb server: #{$!}" + $stderr.puts $!.backtrace + exit(1) +end +DRb.thread.join + +# vim:set filetype=ruby: diff --git a/vendor/plugins/acts_as_ferret/script/ferret_stop b/vendor/plugins/acts_as_ferret/script/ferret_stop new file mode 100755 index 0000000..63b6952 --- /dev/null +++ b/vendor/plugins/acts_as_ferret/script/ferret_stop @@ -0,0 +1,26 @@ +#!/usr/bin/env script/runner + +config = ActsAsFerret::Remote::Config.load + +def send_signal(signal, pid_file) + pid = open(pid_file).read.to_i + print "Sending #{signal} to ferret_server with PID #{pid}..." + begin + Process.kill(signal, pid) + rescue Errno::ESRCH + puts "Process does not exist. Not running. Removing stale pid file anyway." + File.unlink(pid_file) + end + + puts "Done." +end + +pid_file = config['pid_file'] +puts "Stopping ferret_server..." +if File.file?(pid_file) + send_signal("TERM", pid_file) +else + puts "no pid file found" +end + +# vim:set filetype=ruby: diff --git a/vendor/plugins/acts_as_taggable_on_steroids/CHANGELOG b/vendor/plugins/acts_as_taggable_on_steroids/CHANGELOG new file mode 100644 index 0000000..8c36852 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/CHANGELOG @@ -0,0 +1,58 @@ +[1 July 2007] + +* Fix incorrect tagging when the case of the tag list is changed. + +* Fix deprecated Tag.delimiter accessor. + +[23 June 2007] + +* Add validation to Tag model. + +* find_options_for_tagged_with should always return a hash. + +* find_tagged_with passing in no tags should return an empty array. + +* Improve compatibility with PostgreSQL. + +[21 June 2007] + +* Remove extra .rb from generated migration file name. + +[15 June 2007] + +* Introduce TagList class. + +* Various cleanups and improvements. + +* Use TagList.delimiter now, not Tag.delimiter. Tag.delimiter will be removed at some stage. + +[11 June 2007] + +* Restructure the creation of the options for find_tagged_with [Thijs Cadier] + +* Add an example migration with a generator. + +* Add caching. + +* Fix compatibility with Ruby < 1.8.6 + +[23 April 2007] + +* Make tag_list to respect Tag.delimiter + +[31 March 2007] + +* Add Tag.delimiter accessor to change how tags are parsed. +* Fix :include => :tags when used with find_tagged_with + +[7 March 2007] + +* Fix tag_counts for SQLServer [Brad Young] + +[21 Feb 2007] + +* Use scoping instead of TagCountsExtension [Michael Schuerig] + +[7 Jan 2007] + +* Add :match_all to find_tagged_with [Michael Sheakoski] diff --git a/vendor/plugins/acts_as_taggable_on_steroids/MIT-LICENSE b/vendor/plugins/acts_as_taggable_on_steroids/MIT-LICENSE new file mode 100644 index 0000000..602bda2 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2006 Jonathan Viney + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/plugins/acts_as_taggable_on_steroids/README b/vendor/plugins/acts_as_taggable_on_steroids/README new file mode 100644 index 0000000..670d13a --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/README @@ -0,0 +1,116 @@ += acts_as_taggable_on_steroids + +If you find this plugin useful, please consider a donation to show your support! + + http://www.paypal.com/cgi-bin/webscr?cmd=_send-money + + Email address: jonathan.viney@gmail.com + +== Instructions + +This plugin is based on acts_as_taggable by DHH but includes extras +such as tests, smarter tag assignment, and tag cloud calculations. + +Thanks to www.fanacious.com for allowing this plugin to be released. Please check out +their site to show your support. + +== Resources + +Install + * script/plugin install http://svn.viney.net.nz/things/rails/plugins/acts_as_taggable_on_steroids + +== Usage + +=== Prepare database + +Generate and apply the migration: + + ruby script/generate acts_as_taggable_migration + rake db:migrate + +=== Basic tagging + +Using the examples from the tests, let's suppose we have users that have many posts and we want those +posts to be able to be tagged by the user. + +As usual, we add +acts_as_taggable+ to the Post class: + + class Post < ActiveRecord::Base + acts_as_taggable + + belongs_to :user + end + +We can now use the tagging methods provided by acts_as_taggable, tag_list and tag_list=. Both these +methods work like regular attribute accessors. + + p = Post.find(:first) + p.tag_list.to_s # "" + p.tag_list = "Funny, Silly" + p.save + p.reload.tag_list.to_s # "Funny, Silly" + +You can also add or remove arrays of tags. + + p.tag_list.add("Great", "Awful") + p.tag_list.remove("Funny") + +=== Finding tagged objects + +To retrieve objects tagged with a certain tag, use find_tagged_with. + + Post.find_tagged_with('Funny, Silly') + +By default, find_tagged_with will find objects that have any of the given tags. To +find only objects that are tagged with all the given tags, use match_all. + + Post.find_tagged_with('Funny, Silly', :match_all => true) + +=== Tag cloud calculations + +To construct tag clouds, the frequency of each tag needs to be calculated. +Because we specified +acts_as_taggable+ on the Post class, we can +get a calculation of all the tag counts by using Post.tag_counts. But what if we wanted a tag count for +an single user's posts? To achieve this we call tag_counts on the association: + + User.find(:first).posts.tag_counts + +=== Caching + +It is useful to cache the list of tags to reduce the number of queries executed. To do this, +add a column named cached_tag_list to the model which is being tagged. + + class CachePostTagList < ActiveRecord::Migration + def self.up + # You should make sure that the column is long enough to hold + # the full tag list. In some situations the :text type may be more appropriate. + add_column :posts, :cached_tag_list, :string + end + end + + class Post < ActiveRecord::Base + acts_as_taggable + + # The caching column defaults to cached_tag_list, but can be changed: + # + # set_cached_tag_list_column_name "my_caching_column_name" + end + +The details of the caching are handled for you. Just continue to use the tag_list accessor as you normally would. +Note that the cached tag list will not be updated if you directly create Tagging objects or manually append to the +tags or taggings associations. To update the cached tag list you should call save_cached_tag_list manually. + +=== Delimiter + +If you want to change the delimiter used to parse and present tags, set TagList.delimiter. +For example, to use spaces instead of commas, add the following to config/environment.rb: + + TagList.delimiter = " " + +=== Other + +Problems, comments, and suggestions all welcome. jonathan.viney@gmail.com + +== Credits + +www.fanacious.com diff --git a/vendor/plugins/acts_as_taggable_on_steroids/Rakefile b/vendor/plugins/acts_as_taggable_on_steroids/Rakefile new file mode 100644 index 0000000..d2c0003 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/Rakefile @@ -0,0 +1,22 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the acts_as_taggable_on_steroids plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the acts_as_taggable_on_steroids plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'Acts As Taggable On Steroids' + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/acts_as_taggable_migration_generator.rb b/vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/acts_as_taggable_migration_generator.rb new file mode 100644 index 0000000..be9b39c --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/acts_as_taggable_migration_generator.rb @@ -0,0 +1,11 @@ +class ActsAsTaggableMigrationGenerator < Rails::Generator::Base + def manifest + record do |m| + m.migration_template 'migration.rb', 'db/migrate' + end + end + + def file_name + "acts_as_taggable_migration" + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/templates/migration.rb b/vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/templates/migration.rb new file mode 100644 index 0000000..ea0c2cc --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/templates/migration.rb @@ -0,0 +1,26 @@ +class ActsAsTaggableMigration < ActiveRecord::Migration + def self.up + create_table :tags do |t| + t.column :name, :string + end + + create_table :taggings do |t| + t.column :tag_id, :integer + t.column :taggable_id, :integer + + # You should make sure that the column created is + # long enough to store the required class names. + t.column :taggable_type, :string + + t.column :created_at, :datetime + end + + add_index :taggings, :tag_id + add_index :taggings, [:taggable_id, :taggable_type] + end + + def self.down + drop_table :taggings + drop_table :tags + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/init.rb b/vendor/plugins/acts_as_taggable_on_steroids/init.rb new file mode 100644 index 0000000..5d3aa8e --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/init.rb @@ -0,0 +1,4 @@ +require File.dirname(__FILE__) + '/lib/acts_as_taggable' + +require File.dirname(__FILE__) + '/lib/tagging' +require File.dirname(__FILE__) + '/lib/tag' diff --git a/vendor/plugins/acts_as_taggable_on_steroids/lib/acts_as_taggable.rb b/vendor/plugins/acts_as_taggable_on_steroids/lib/acts_as_taggable.rb new file mode 100644 index 0000000..0d424fe --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/lib/acts_as_taggable.rb @@ -0,0 +1,145 @@ +module ActiveRecord + module Acts #:nodoc: + module Taggable #:nodoc: + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def acts_as_taggable(options = {}) + has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag + has_many :tags, :through => :taggings + + before_save :save_cached_tag_list + after_save :save_tags + + include ActiveRecord::Acts::Taggable::InstanceMethods + extend ActiveRecord::Acts::Taggable::SingletonMethods + + alias_method :reload_without_tag_list, :reload + alias_method :reload, :reload_with_tag_list + end + + def cached_tag_list_column_name + "cached_tag_list" + end + + def set_cached_tag_list_column_name(value = nil, &block) + define_attr_method :cached_tag_list_column_name, value, &block + end + end + + module SingletonMethods + # Pass either a tag string, or an array of strings or tags + # + # Options: + # :exclude - Find models that are not tagged with the given tags + # :match_all - Find models that match all of the given tags, not just one + # :conditions - A piece of SQL conditions to add to the query + def find_tagged_with(tags, options = {}) + tags = TagList.from(tags).names + return [] if tags.empty? + + conditions = tags.map {|t| sanitize_sql(["tags.name LIKE ?",t])}.join(' OR ') + conditions += ' AND ' + sanitize_sql(options.delete(:conditions)) if options[:conditions] + group = "#{table_name}.id HAVING COUNT(#{table_name}.id) = #{tags.size}" if options.delete(:match_all) + exclude = options.delete(:exclude) + taggeds = find(:all, {:conditions => conditions, :include => :tags, :group => group}.update(options)) + exclude ? find(:all) - taggeds : taggeds + end + + # Options: + # :start_at - Restrict the tags to those created after a certain time + # :end_at - Restrict the tags to those created before a certain time + # :conditions - A piece of SQL conditions to add to the query + # :limit - The maximum number of tags to return + # :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc' + # :at_least - Exclude tags with a frequency less than the given value + # :at_most - Exclude tags with a frequency greater then the given value + def tag_counts(*args) + Tag.find(:all, find_options_for_tag_counts(*args)) + end + + def find_options_for_tag_counts(options = {}) + options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit + + scope = scope(:find) + start_at = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options[:start_at]]) if options[:start_at] + end_at = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options[:end_at]]) if options[:end_at] + + conditions = [ + "#{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)}", + options[:conditions], + scope && scope[:conditions], + start_at, + end_at + ] + conditions = conditions.compact.join(' and ') + + at_least = sanitize_sql(['COUNT(*) >= ?', options[:at_least]]) if options[:at_least] + at_most = sanitize_sql(['COUNT(*) <= ?', options[:at_most]]) if options[:at_most] + having = [at_least, at_most].compact.join(' and ') + group_by = "#{Tag.table_name}.id, #{Tag.table_name}.name HAVING COUNT(*) > 0" + group_by << " AND #{having}" unless having.blank? + + { :select => "#{Tag.table_name}.id, #{Tag.table_name}.name, COUNT(*) AS count", + :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", + :conditions => conditions, + :group => group_by, + :order => options[:order], + :limit => options[:limit] + } + end + end + + module InstanceMethods + def tag_list + if @tag_list + @tag_list + elsif caching_tag_list? and !send(self.class.cached_tag_list_column_name).nil? + @tag_list = TagList.from(send(self.class.cached_tag_list_column_name)) + else + @tag_list = TagList.new(tags.map(&:name)) + end + end + + def tag_list=(value) + @tag_list = TagList.from(value) + end + + def save_cached_tag_list + if caching_tag_list? and !tag_list.blank? + self[self.class.cached_tag_list_column_name] = tag_list.to_s + end + end + + def save_tags + return unless @tag_list + + new_tag_names = @tag_list.names - tags.map(&:name) + old_tags = tags.reject { |tag| @tag_list.names.include?(tag.name) } + + self.class.transaction do + tags.delete(*old_tags) if old_tags.any? + + new_tag_names.each do |new_tag_name| + tags << (Tag.find(:first, :conditions => ['name like ?',new_tag_name]) || Tag.create(:name => new_tag_name)) + end + end + true + end + + def reload_with_tag_list(*args) + @tag_list = nil + reload_without_tag_list(*args) + end + + def caching_tag_list? + self.class.column_names.include?(self.class.cached_tag_list_column_name) + end + end + end + end +end + +ActiveRecord::Base.send(:include, ActiveRecord::Acts::Taggable) diff --git a/vendor/plugins/acts_as_taggable_on_steroids/lib/tag.rb b/vendor/plugins/acts_as_taggable_on_steroids/lib/tag.rb new file mode 100644 index 0000000..41b4e3c --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/lib/tag.rb @@ -0,0 +1,39 @@ +class Tag < ActiveRecord::Base + has_many :taggings + + validates_presence_of :name + validates_uniqueness_of :name + + class << self + delegate :delimiter, :delimiter=, :to => TagList + end + + def ==(object) + super || (object.is_a?(Tag) && name == object.name) + end + + def to_s + name + end + + def count + read_attribute(:count).to_i + end + + def self.hierarchical=(bool) + if bool + acts_as_tree + end + end + + # All the tags that can be a new parent for this tag, that is all but itself and its descendents to avoid loops + def parent_candidates + Tag.find_all_by_pending(false) - descendents - [self] + end + + # All tags that have this tag as its one of its ancestors + def descendents + children.to_a.sum([], &:descendents) + children + end + +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/lib/tag_counts_extension.rb b/vendor/plugins/acts_as_taggable_on_steroids/lib/tag_counts_extension.rb new file mode 100644 index 0000000..01defe0 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/lib/tag_counts_extension.rb @@ -0,0 +1,2 @@ +module TagCountsExtension +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/lib/tag_list.rb b/vendor/plugins/acts_as_taggable_on_steroids/lib/tag_list.rb new file mode 100644 index 0000000..30093be --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/lib/tag_list.rb @@ -0,0 +1,66 @@ +class TagList + cattr_accessor :delimiter + self.delimiter = ',' + + attr_reader :names + + def initialize(*names) + @names = [] + add(*names) + end + + def add(*names) + names = names.flatten + + # Strip whitespace and remove blank or duplicate tags + names.map!(&:strip) + names.reject!(&:blank?) + + @names.concat(names) + @names.uniq! + end + + def remove(*names) + names = names.flatten + @names.delete_if { |name| names.include?(name) } + end + + def blank? + @names.empty? + end + + def to_s + @names.map do |name| + name.include?(delimiter) ? "\"#{name}\"" : name + end.join(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ") + end + + def ==(other) + super || (other.is_a?(TagList) && other.names == @names) + end + + class << self + def from(tags) + case tags + when String + new(parse(tags)) + when Array + new(tags.map(&:to_s)) + else + new([]) + end + end + + def parse(string) + returning [] do |names| + string = string.to_s.dup + + # Parse the quoted tags + string.gsub!(/"(.*?)"\s*#{delimiter}?\s*/) { names << $1; "" } + string.gsub!(/'(.*?)'\s*#{delimiter}?\s*/) { names << $1; "" } + + names.concat(string.split(delimiter)) + end + end + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/lib/tagging.rb b/vendor/plugins/acts_as_taggable_on_steroids/lib/tagging.rb new file mode 100644 index 0000000..33daf86 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/lib/tagging.rb @@ -0,0 +1,4 @@ +class Tagging < ActiveRecord::Base + belongs_to :tag + belongs_to :taggable, :polymorphic => true +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/abstract_unit.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/abstract_unit.rb new file mode 100644 index 0000000..5b36d7b --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/abstract_unit.rb @@ -0,0 +1,82 @@ +require 'test/unit' + +begin + require File.dirname(__FILE__) + '/../../../../config/environment' +rescue LoadError + require 'rubygems' + require_gem 'activerecord' + require_gem 'actionpack' +end + +# Search for fixtures first +fixture_path = File.dirname(__FILE__) + '/fixtures/' +begin + Dependencies.load_paths.insert(0, fixture_path) +rescue + $LOAD_PATH.unshift(fixture_path) +end + +require 'active_record/fixtures' + +require File.dirname(__FILE__) + '/../lib/acts_as_taggable' +require_dependency File.dirname(__FILE__) + '/../lib/tag_list' + +ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + '/debug.log') +#ActiveRecord::Base.configurations = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml')) +#ActiveRecord::Base.establish_connection(ENV['DB'] || 'mysql') + +load(File.dirname(__FILE__) + '/schema.rb') + +Test::Unit::TestCase.fixture_path = fixture_path + +class Test::Unit::TestCase #:nodoc: + self.use_transactional_fixtures = true + self.use_instantiated_fixtures = false + + def assert_equivalent(expected, actual, message = nil) + if expected.first.is_a?(ActiveRecord::Base) + assert_equal expected.sort_by(&:id), actual.sort_by(&:id), message + else + assert_equal expected.sort, actual.sort, message + end + end + + def assert_tag_counts(tags, expected_values) + # Map the tag fixture names to real tag names + expected_values = expected_values.inject({}) do |hash, (tag, count)| + hash[tags(tag).name] = count + hash + end + + tags.each do |tag| + value = expected_values.delete(tag.name) + assert_not_nil value, "Expected count for #{tag.name} was not provided" if value.nil? + assert_equal value, tag.count, "Expected value of #{value} for #{tag.name}, but was #{tag.count}" + end + + unless expected_values.empty? + assert false, "The following tag counts were not present: #{expected_values.inspect}" + end + end + + def assert_queries(num = 1) + $query_count = 0 + yield + ensure + assert_equal num, $query_count, "#{$query_count} instead of #{num} queries were executed." + end + + def assert_no_queries(&block) + assert_queries(0, &block) + end +end + +ActiveRecord::Base.connection.class.class_eval do + def execute_with_counting(sql, name = nil, &block) + $query_count ||= 0 + $query_count += 1 + execute_without_counting(sql, name, &block) + end + + alias_method_chain :execute, :counting +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/acts_as_taggable_test.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/acts_as_taggable_test.rb new file mode 100644 index 0000000..d8f3c09 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/acts_as_taggable_test.rb @@ -0,0 +1,272 @@ +require File.dirname(__FILE__) + '/abstract_unit' + +class ActsAsTaggableOnSteroidsTest < Test::Unit::TestCase + fixtures :tags, :taggings, :posts, :users, :photos + + def test_find_tagged_with + assert_equivalent [posts(:jonathan_sky), posts(:sam_flowers)], Post.find_tagged_with('"Very good"') + assert_equal Post.find_tagged_with('"Very good"'), Post.find_tagged_with(['Very good']) + assert_equal Post.find_tagged_with('"Very good"'), Post.find_tagged_with([tags(:good)]) + + assert_equivalent [photos(:jonathan_dog), photos(:sam_flower), photos(:sam_sky)], Photo.find_tagged_with('Nature') + assert_equal Photo.find_tagged_with('Nature'), Photo.find_tagged_with(['Nature']) + assert_equal Photo.find_tagged_with('Nature'), Photo.find_tagged_with([tags(:nature)]) + + assert_equivalent [photos(:jonathan_bad_cat), photos(:jonathan_dog), photos(:jonathan_questioning_dog)], Photo.find_tagged_with('"Crazy animal" Bad') + assert_equal Photo.find_tagged_with('"Crazy animal" Bad'), Photo.find_tagged_with(['Crazy animal', 'Bad']) + assert_equal Photo.find_tagged_with('"Crazy animal" Bad'), Photo.find_tagged_with([tags(:animal), tags(:bad)]) + end + + def test_find_tagged_with_nothing + assert_equal [], Post.find_tagged_with("") + assert_equal [], Post.find_tagged_with([]) + end + + def test_find_tagged_with_nonexistant_tags + assert_equal [], Post.find_tagged_with('ABCDEFG') + assert_equal [], Photo.find_tagged_with(['HIJKLM']) + assert_equal [], Photo.find_tagged_with([Tag.new(:name => 'unsaved tag')]) + end + + def test_find_tagged_with_matching_all_tags + assert_equivalent [photos(:jonathan_dog)], Photo.find_tagged_with('Crazy animal, "Nature"', :match_all => true) + assert_equivalent [posts(:jonathan_sky), posts(:sam_flowers)], Post.find_tagged_with(['Very good', 'Nature'], :match_all => true) + end + + def test_find_tagged_with_exclusions + assert_equivalent [photos(:jonathan_questioning_dog), photos(:jonathan_bad_cat)], Photo.find_tagged_with("Nature", :exclude => true) + assert_equivalent [posts(:jonathan_grass), posts(:jonathan_rain)], Post.find_tagged_with("'Very good', Bad", :exclude => true) + end + +# def test_find_options_for_tagged_with_no_tags_returns_empty_hash +# assert_equal Hash.new, Post.find_options_for_tagged_with("") +# assert_equal Hash.new, Post.find_options_for_tagged_with([nil]) +# end + +# def test_find_options_for_tagged_with_leavs_arguments_unchanged +# original_tags = photos(:jonathan_questioning_dog).tags.dup +# Photo.find_options_for_tagged_with(photos(:jonathan_questioning_dog).tags) +# assert_equal original_tags, photos(:jonathan_questioning_dog).tags +# end + +# def test_find_options_for_tagged_with_respects_custom_table_name +# Tagging.table_name = "categorisations" +# Tag.table_name = "categories" +# +# options = Photo.find_options_for_tagged_with("Hello") +# +# assert_no_match Regexp.new(" taggings "), options[:joins] +# assert_no_match Regexp.new(" tags "), options[:joins] +# +# assert_match Regexp.new(" categorisations "), options[:joins] +# assert_match Regexp.new(" categories "), options[:joins] +# ensure +# Tagging.table_name = "taggings" +# Tag.table_name = "tags" +# end + + def test_include_tags_on_find_tagged_with + assert_nothing_raised do + Photo.find_tagged_with('Nature', :include => :tags) + Photo.find_tagged_with("Nature", :include => { :taggings => :tag }) + end + end + + def test_basic_tag_counts_on_class + assert_tag_counts Post.tag_counts, :good => 2, :nature => 5, :question => 1, :bad => 1 + assert_tag_counts Photo.tag_counts, :good => 1, :nature => 3, :question => 1, :bad => 1, :animal => 3 + end + + def test_tag_counts_on_class_with_date_conditions + assert_tag_counts Post.tag_counts(:start_at => Date.new(2006, 8, 4)), :good => 1, :nature => 3, :question => 1, :bad => 1 + assert_tag_counts Post.tag_counts(:end_at => Date.new(2006, 8, 6)), :good => 1, :nature => 4, :question => 1 + 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 + + 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 + end + + def test_tag_counts_on_class_with_frequencies + assert_tag_counts Photo.tag_counts(:at_least => 2), :nature => 3, :animal => 3 + assert_tag_counts Photo.tag_counts(:at_most => 2), :good => 1, :question => 1, :bad => 1 + end + + def test_tag_counts_with_limit + assert_equal 2, Photo.tag_counts(:limit => 2).size + assert_equal 1, Post.tag_counts(:at_least => 4, :limit => 2).size + end + + def test_tag_counts_with_limit_and_order + assert_equal [tags(:nature), tags(:good)], Post.tag_counts(:order => 'count desc', :limit => 2) + end + + def test_tag_counts_on_association + assert_tag_counts users(:jonathan).posts.tag_counts, :good => 1, :nature => 3, :question => 1 + assert_tag_counts users(:sam).posts.tag_counts, :good => 1, :nature => 2, :bad => 1 + + assert_tag_counts users(:jonathan).photos.tag_counts, :animal => 3, :nature => 1, :question => 1, :bad => 1 + assert_tag_counts users(:sam).photos.tag_counts, :nature => 2, :good => 1 + end + + def test_tag_counts_on_association_with_options + assert_equal [], users(:jonathan).posts.tag_counts(:conditions => '1=0') + assert_tag_counts users(:jonathan).posts.tag_counts(:at_most => 2), :good => 1, :question => 1 + end + + def test_tag_counts_respects_custom_table_names + Tagging.table_name = "categorisations" + Tag.table_name = "categories" + + options = Photo.find_options_for_tag_counts(:start_at => 2.weeks.ago, :end_at => Date.today) + sql = options.values.join(' ') + + assert_no_match /taggings/, sql + assert_no_match /tags/, sql + + assert_match /categorisations/, sql + assert_match /categories/, sql + ensure + Tagging.table_name = "taggings" + Tag.table_name = "tags" + end + + def test_tag_list_reader + assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list.names + assert_equivalent ["Bad", "Crazy animal"], photos(:jonathan_bad_cat).tag_list.names + end + + def test_reassign_tag_list + assert_equivalent ["Nature", "Question"], posts(:jonathan_rain).tag_list.names + posts(:jonathan_rain).taggings.reload + + # Only an update of the posts table should be executed + assert_queries 1 do + posts(:jonathan_rain).update_attributes!(:tag_list => posts(:jonathan_rain).tag_list.to_s) + end + + assert_equivalent ["Nature", "Question"], posts(:jonathan_rain).tag_list.names + end + + def test_new_tags + assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list.names + posts(:jonathan_sky).update_attributes!(:tag_list => "#{posts(:jonathan_sky).tag_list}, One, Two") + assert_equivalent ["Very good", "Nature", "One", "Two"], posts(:jonathan_sky).tag_list.names + end + + def test_remove_tag + assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list.names + posts(:jonathan_sky).update_attributes!(:tag_list => "Nature") + assert_equivalent ["Nature"], posts(:jonathan_sky).tag_list.names + end + + def test_change_case_of_tags + original_tag_names = photos(:jonathan_questioning_dog).tag_list.names + photos(:jonathan_questioning_dog).update_attributes!(:tag_list => photos(:jonathan_questioning_dog).tag_list.to_s.upcase) + + # The new tag list is not uppercase becuase the AR finders are not case-sensitive + # and find the old tags when re-tagging with the uppercase tags. + assert_equivalent original_tag_names, photos(:jonathan_questioning_dog).reload.tag_list.names + end + + def test_remove_and_add_tag + assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list.names + posts(:jonathan_sky).update_attributes!(:tag_list => "Nature, Beautiful") + assert_equivalent ["Nature", "Beautiful"], posts(:jonathan_sky).tag_list.names + end + + def test_tags_not_saved_if_validation_fails + assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list.names + assert !posts(:jonathan_sky).update_attributes(:tag_list => "One, Two", :text => "") + assert_equivalent ["Very good", "Nature"], Post.find(posts(:jonathan_sky).id).tag_list.names + end + + def test_tag_list_accessors_on_new_record + p = Post.new(:text => 'Test') + + assert p.tag_list.blank? + p.tag_list = "One, Two" + assert_equal "One, Two", p.tag_list.to_s + end + + def test_clear_tag_list_with_nil + p = photos(:jonathan_questioning_dog) + + assert !p.tag_list.blank? + assert p.update_attributes(:tag_list => nil) + assert p.tag_list.blank? + + assert p.reload.tag_list.blank? + end + + def test_clear_tag_list_with_string + p = photos(:jonathan_questioning_dog) + + assert !p.tag_list.blank? + assert p.update_attributes(:tag_list => ' ') + assert p.tag_list.blank? + + assert p.reload.tag_list.blank? + end + + def test_tag_list_reset_on_reload + p = photos(:jonathan_questioning_dog) + assert !p.tag_list.blank? + p.tag_list = nil + assert p.tag_list.blank? + assert !p.reload.tag_list.blank? + end + + def test_tag_list_populated_when_cache_nil + assert_nil posts(:jonathan_sky).cached_tag_list + posts(:jonathan_sky).save! + assert_equal posts(:jonathan_sky).tag_list.to_s, posts(:jonathan_sky).cached_tag_list + end + + def test_cached_tag_list_used + posts(:jonathan_sky).save! + posts(:jonathan_sky).reload + + assert_no_queries do + assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list.names + end + end + + def test_cached_tag_list_not_used + # Load fixture and column information + posts(:jonathan_sky).taggings(:reload) + + assert_queries 1 do + # Tags association will be loaded + posts(:jonathan_sky).tag_list + end + end + + def test_cached_tag_list_updated + assert_nil posts(:jonathan_sky).cached_tag_list + posts(:jonathan_sky).save! + assert_equivalent ["Very good", "Nature"], TagList.from(posts(:jonathan_sky).cached_tag_list).names + posts(:jonathan_sky).update_attributes!(:tag_list => "None") + + assert_equal 'None', posts(:jonathan_sky).cached_tag_list + assert_equal 'None', posts(:jonathan_sky).reload.cached_tag_list + end + + def test_inherited_taggable + # SpPost inherits acts_as_taggable from its ancestor Post + p = SpPost.new(:text => 'bla bla bla ...') + p.tag_list = 'bla' + p.save + assert !SpPost.find_tagged_with('bla').blank? + end +end + +class ActsAsTaggableOnSteroidsFormTest < Test::Unit::TestCase + fixtures :tags, :taggings, :posts, :users, :photos + + include ActionView::Helpers::FormHelper + + def test_tag_list_contents + fields_for :post, posts(:jonathan_sky) do |f| + assert_match /Very good, Nature/, f.text_field(:tag_list) + end + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/database.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/database.yml new file mode 100644 index 0000000..0f484ae --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/database.yml @@ -0,0 +1,10 @@ +mysql: + adapter: mysql + host: localhost + username: rails + password: + database: rails_plugin_test + +sqlite3: + adapter: sqlite3 + database: ':memory:' diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photo.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photo.rb new file mode 100644 index 0000000..224957f --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photo.rb @@ -0,0 +1,8 @@ +class Photo < ActiveRecord::Base + acts_as_taggable + + belongs_to :user +end + +class SpecialPhoto < Photo +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photos.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photos.yml new file mode 100644 index 0000000..25a4118 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photos.yml @@ -0,0 +1,24 @@ +jonathan_dog: + id: 1 + user_id: 1 + title: A small dog + +jonathan_questioning_dog: + id: 2 + user_id: 1 + title: What does this dog want? + +jonathan_bad_cat: + id: 3 + user_id: 1 + title: Bad cat + +sam_flower: + id: 4 + user_id: 2 + title: Flower + +sam_sky: + id: 5 + user_id: 2 + title: Sky diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/post.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/post.rb new file mode 100644 index 0000000..bee100a --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/post.rb @@ -0,0 +1,7 @@ +class Post < ActiveRecord::Base + acts_as_taggable + + belongs_to :user + + validates_presence_of :text +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/posts.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/posts.yml new file mode 100644 index 0000000..d0cd9ac --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/posts.yml @@ -0,0 +1,24 @@ +jonathan_sky: + id: 1 + user_id: 1 + text: The sky is particularly blue today + +jonathan_grass: + id: 2 + user_id: 1 + text: The grass seems very green + +jonathan_rain: + id: 3 + user_id: 1 + text: Why does the rain fall? + +sam_ground: + id: 4 + user_id: 2 + text: The ground is looking too brown + +sam_flowers: + id: 5 + user_id: 2 + text: Why are the flowers dead? \ No newline at end of file diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/sp_post.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/sp_post.rb new file mode 100644 index 0000000..5418a92 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/sp_post.rb @@ -0,0 +1,5 @@ +class SpPost < Post + def ihnerited + true + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/taggings.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/taggings.yml new file mode 100644 index 0000000..b6eb440 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/taggings.yml @@ -0,0 +1,126 @@ +jonathan_sky_good: + id: 1 + tag_id: 1 + taggable_id: 1 + taggable_type: Post + created_at: 2006-08-01 + +jonathan_sky_nature: + id: 2 + tag_id: 3 + taggable_id: 1 + taggable_type: Post + created_at: 2006-08-02 + +jonathan_grass_nature: + id: 3 + tag_id: 3 + taggable_id: 2 + taggable_type: Post + created_at: 2006-08-03 + +jonathan_rain_question: + id: 4 + tag_id: 4 + taggable_id: 3 + taggable_type: Post + created_at: 2006-08-04 + +jonathan_rain_nature: + id: 5 + tag_id: 3 + taggable_id: 3 + taggable_type: Post + created_at: 2006-08-05 + +sam_ground_nature: + id: 6 + tag_id: 3 + taggable_id: 4 + taggable_type: Post + created_at: 2006-08-06 + +sam_ground_bad: + id: 7 + tag_id: 2 + taggable_id: 4 + taggable_type: Post + created_at: 2006-08-07 + +sam_flowers_good: + id: 8 + tag_id: 1 + taggable_id: 5 + taggable_type: Post + created_at: 2006-08-08 + +sam_flowers_nature: + id: 9 + tag_id: 3 + taggable_id: 5 + taggable_type: Post + created_at: 2006-08-09 + + +jonathan_dog_animal: + id: 10 + tag_id: 5 + taggable_id: 1 + taggable_type: Photo + created_at: 2006-08-10 + +jonathan_dog_nature: + id: 11 + tag_id: 3 + taggable_id: 1 + taggable_type: Photo + created_at: 2006-08-11 + +jonathan_questioning_dog_animal: + id: 12 + tag_id: 5 + taggable_id: 2 + taggable_type: Photo + created_at: 2006-08-12 + +jonathan_questioning_dog_question: + id: 13 + tag_id: 4 + taggable_id: 2 + taggable_type: Photo + created_at: 2006-08-13 + +jonathan_bad_cat_bad: + id: 14 + tag_id: 2 + taggable_id: 3 + taggable_type: Photo + created_at: 2006-08-14 + +jonathan_bad_cat_animal: + id: 15 + tag_id: 5 + taggable_id: 3 + taggable_type: Photo + created_at: 2006-08-15 + +sam_flower_nature: + id: 16 + tag_id: 3 + taggable_id: 4 + taggable_type: Photo + created_at: 2006-08-16 + +sam_flower_good: + id: 17 + tag_id: 1 + taggable_id: 4 + taggable_type: Photo + created_at: 2006-08-17 + +sam_sky_nature: + id: 18 + tag_id: 3 + taggable_id: 5 + taggable_type: Photo + created_at: 2006-08-18 diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/tags.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/tags.yml new file mode 100644 index 0000000..b8f8367 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/tags.yml @@ -0,0 +1,19 @@ +good: + id: 1 + name: Very good + +bad: + id: 2 + name: Bad + +nature: + id: 3 + name: Nature + +question: + id: 4 + name: Question + +animal: + id: 5 + name: Crazy animal \ No newline at end of file diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/user.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/user.rb new file mode 100644 index 0000000..c85a292 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/user.rb @@ -0,0 +1,4 @@ +class User < ActiveRecord::Base + has_many :posts + has_many :photos +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/users.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/users.yml new file mode 100644 index 0000000..da94fea --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/users.yml @@ -0,0 +1,7 @@ +jonathan: + id: 1 + name: Jonathan + +sam: + id: 2 + name: Sam diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/schema.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/schema.rb new file mode 100644 index 0000000..89680c0 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/schema.rb @@ -0,0 +1,31 @@ +ActiveRecord::Schema.define :version => 0 do + create_table :tags, :force => true do |t| + t.column :name, :string + t.column :parent_id, :integer + t.column :pending, :boolean + end + + create_table :taggings, :force => true do |t| + t.column :tag_id, :integer + t.column :taggable_id, :integer + t.column :taggable_type, :string + t.column :created_at, :datetime + end + + create_table :users, :force => true do |t| + t.column :name, :string + end + + create_table :posts, :force => true do |t| + t.column :text, :text + t.column :cached_tag_list, :string + t.column :user_id, :integer + + t.column :type, :string + end + + create_table :photos, :force => true do |t| + t.column :title, :string + t.column :user_id, :integer + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/tag_list_test.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/tag_list_test.rb new file mode 100644 index 0000000..58b2216 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/tag_list_test.rb @@ -0,0 +1,98 @@ +require File.dirname(__FILE__) + '/abstract_unit' + +class TagListTest < Test::Unit::TestCase + def test_blank? + assert TagList.new.blank? + end + + def test_equality + assert_equal TagList.new, TagList.new + assert_equal TagList.new("tag"), TagList.new("tag") + + assert_not_equal TagList.new, "" + assert_not_equal TagList.new, TagList.new("tag") + end + + def test_parse_leaves_string_unchanged + tags = '"one ", two' + original = tags.dup + TagList.parse(tags) + assert_equal tags, original + end + + def test_from_single_name + assert_equal %w(fun), TagList.from("fun").names + assert_equal %w(fun), TagList.from('"fun"').names + end + + def test_from_blank + assert_equal [], TagList.from(nil).names + assert_equal [], TagList.from("").names + end + + def test_from_single_quoted_tag + assert_equal ['with, comma'], TagList.from('"with, comma"').names + end + + def test_spaces_do_not_delineate + assert_equal ['a b', 'c'], TagList.from('a b, c').names + end + + def test_from_multiple_tags + assert_equivalent %w(alpha beta delta gamma), TagList.from("alpha, beta, delta, gamma").names.sort + end + + def test_from_multiple_tags_with_quotes + assert_equivalent %w(alpha beta delta gamma), TagList.from('alpha, "beta", gamma , "delta"').names.sort + end + + def test_from_multiple_tags_with_quote_and_commas + assert_equivalent ['alpha, beta', 'delta', 'gamma, something'], TagList.from('"alpha, beta", delta, "gamma, something"').names + end + + def test_from_removes_white_space + assert_equivalent %w(alpha beta), TagList.from('" alpha ", "beta "').names + assert_equivalent %w(alpha beta), TagList.from(' alpha, beta ').names + end + + def test_alternative_delimiter + TagList.delimiter = " " + + assert_equal %w(one two), TagList.from("one two").names + assert_equal ['one two', 'three', 'four'], TagList.from('"one two" three four').names + ensure + TagList.delimiter = "," + end + + def test_duplicate_tags_removed + assert_equal %w(one), TagList.from("one, one").names + end + + def test_to_s_with_commas + assert_equal "question, crazy animal", TagList.new(["question", "crazy animal"]).to_s + end + + def test_to_s_with_alternative_delimiter + TagList.delimiter = " " + + assert_equal '"crazy animal" question', TagList.new(["crazy animal", "question"]).to_s + ensure + TagList.delimiter = "," + end + + def test_add + tag_list = TagList.new("one") + assert_equal %w(one), tag_list.names + + tag_list.add("two") + assert_equal %w(one two), tag_list.names + end + + def test_remove + tag_list = TagList.new("one", "two") + assert_equal %w(one two), tag_list.names + + tag_list.remove("one") + assert_equal %w(two), tag_list.names + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/tag_test.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/tag_test.rb new file mode 100644 index 0000000..e383103 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/tag_test.rb @@ -0,0 +1,42 @@ +require File.dirname(__FILE__) + '/abstract_unit' + +class TagTest < Test::Unit::TestCase + fixtures :tags, :taggings, :users, :photos, :posts + + def test_name_required + t = Tag.create + assert_match /blank/, t.errors[:name].to_s + end + + def test_name_unique + t = Tag.create!(:name => "My tag") + duplicate = t.clone + + assert !duplicate.save + assert_match /taken/, duplicate.errors[:name].to_s + end + + def test_taggings + assert_equivalent [taggings(:jonathan_sky_good), taggings(:sam_flowers_good), taggings(:sam_flower_good)], tags(:good).taggings + assert_equivalent [taggings(:sam_ground_bad), taggings(:jonathan_bad_cat_bad)], tags(:bad).taggings + end + + def test_to_s + assert_equal tags(:good).name, tags(:good).to_s + end + + def test_equality + assert_equal tags(:good), tags(:good) + assert_equal Tag.find(1), Tag.find(1) + assert_equal Tag.new(:name => 'A'), Tag.new(:name => 'A') + assert_not_equal Tag.new(:name => 'A'), Tag.new(:name => 'B') + end + + def test_deprecated_delimiter + original_delimiter = Tag.delimiter + Tag.delimiter = ":" + assert_equal ":", TagList.delimiter + ensure + TagList.delimiter = original_delimiter + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/tagging_test.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/tagging_test.rb new file mode 100644 index 0000000..172b8e2 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/tagging_test.rb @@ -0,0 +1,13 @@ +require File.dirname(__FILE__) + '/abstract_unit' + +class TaggingTest < Test::Unit::TestCase + fixtures :tags, :taggings, :posts + + def test_tag + assert_equal tags(:good), taggings(:jonathan_sky_good).tag + end + + def test_taggable + assert_equal posts(:jonathan_sky), taggings(:jonathan_sky_good).taggable + end +end diff --git a/vendor/plugins/acts_as_versioned/CHANGELOG b/vendor/plugins/acts_as_versioned/CHANGELOG new file mode 100644 index 0000000..a5d339c --- /dev/null +++ b/vendor/plugins/acts_as_versioned/CHANGELOG @@ -0,0 +1,74 @@ +*SVN* (version numbers are overrated) + +* (5 Oct 2006) Allow customization of #versions association options [Dan Peterson] + +*0.5.1* + +* (8 Aug 2006) Versioned models now belong to the unversioned model. @article_version.article.class => Article [Aslak Hellesoy] + +*0.5* # do versions even matter for plugins? + +* (21 Apr 2006) Added without_locking and without_revision methods. + + Foo.without_revision do + @foo.update_attributes ... + end + +*0.4* + +* (28 March 2006) Rename non_versioned_fields to non_versioned_columns (old one is kept for compatibility). +* (28 March 2006) Made explicit documentation note that string column names are required for non_versioned_columns. + +*0.3.1* + +* (7 Jan 2006) explicitly set :foreign_key option for the versioned model's belongs_to assocation for STI [Caged] +* (7 Jan 2006) added tests to prove has_many :through joins work + +*0.3* + +* (2 Jan 2006) added ability to share a mixin with versioned class +* (2 Jan 2006) changed the dynamic version model to MyModel::Version + +*0.2.4* + +* (27 Nov 2005) added note about possible destructive behavior of if_changed? [Michael Schuerig] + +*0.2.3* + +* (12 Nov 2005) fixed bug with old behavior of #blank? [Michael Schuerig] +* (12 Nov 2005) updated tests to use ActiveRecord Schema + +*0.2.2* + +* (3 Nov 2005) added documentation note to #acts_as_versioned [Martin Jul] + +*0.2.1* + +* (6 Oct 2005) renamed dirty? to changed? to keep it uniform. it was aliased to keep it backwards compatible. + +*0.2* + +* (6 Oct 2005) added find_versions and find_version class methods. + +* (6 Oct 2005) removed transaction from create_versioned_table(). + this way you can specify your own transaction around a group of operations. + +* (30 Sep 2005) fixed bug where find_versions() would order by 'version' twice. (found by Joe Clark) + +* (26 Sep 2005) added :sequence_name option to acts_as_versioned to set the sequence name on the versioned model + +*0.1.3* (18 Sep 2005) + +* First RubyForge release + +*0.1.2* + +* check if module is already included when acts_as_versioned is called + +*0.1.1* + +* Adding tests and rdocs + +*0.1* + +* Initial transfer from Rails ticket: http://dev.rubyonrails.com/ticket/1974 \ No newline at end of file diff --git a/vendor/plugins/acts_as_versioned/MIT-LICENSE b/vendor/plugins/acts_as_versioned/MIT-LICENSE new file mode 100644 index 0000000..5851fda --- /dev/null +++ b/vendor/plugins/acts_as_versioned/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2005 Rick Olson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/vendor/plugins/acts_as_versioned/README b/vendor/plugins/acts_as_versioned/README new file mode 100644 index 0000000..8961f05 --- /dev/null +++ b/vendor/plugins/acts_as_versioned/README @@ -0,0 +1,28 @@ += acts_as_versioned + +This library adds simple versioning to an ActiveRecord module. ActiveRecord is required. + +== Resources + +Install + +* gem install acts_as_versioned + +Rubyforge project + +* http://rubyforge.org/projects/ar-versioned + +RDocs + +* http://ar-versioned.rubyforge.org + +Subversion + +* http://techno-weenie.net/svn/projects/acts_as_versioned + +Collaboa + +* http://collaboa.techno-weenie.net/repository/browse/acts_as_versioned + +Special thanks to Dreamer on ##rubyonrails for help in early testing. His ServerSideWiki (http://serversidewiki.com) +was the first project to use acts_as_versioned in the wild. \ No newline at end of file diff --git a/vendor/plugins/acts_as_versioned/RUNNING_UNIT_TESTS b/vendor/plugins/acts_as_versioned/RUNNING_UNIT_TESTS new file mode 100644 index 0000000..a6e55b8 --- /dev/null +++ b/vendor/plugins/acts_as_versioned/RUNNING_UNIT_TESTS @@ -0,0 +1,41 @@ +== Creating the test database + +The default name for the test databases is "activerecord_versioned". If you +want to use another database name then be sure to update the connection +adapter setups you want to test with in test/connections//connection.rb. +When you have the database online, you can import the fixture tables with +the test/fixtures/db_definitions/*.sql files. + +Make sure that you create database objects with the same user that you specified in i +connection.rb otherwise (on Postgres, at least) tests for default values will fail. + +== Running with Rake + +The easiest way to run the unit tests is through Rake. The default task runs +the entire test suite for all the adapters. You can also run the suite on just +one adapter by using the tasks test_mysql_ruby, test_ruby_mysql, test_sqlite, +or test_postresql. For more information, checkout the full array of rake tasks with "rake -T" + +Rake can be found at http://rake.rubyforge.org + +== Running by hand + +Unit tests are located in test directory. If you only want to run a single test suite, +or don't want to bother with Rake, you can do so with something like: + + cd test; ruby -I "connections/native_mysql" base_test.rb + +That'll run the base suite using the MySQL-Ruby adapter. Change the adapter +and test suite name as needed. + +== Faster tests + +If you are using a database that supports transactions, you can set the +"AR_TX_FIXTURES" environment variable to "yes" to use transactional fixtures. +This gives a very large speed boost. With rake: + + rake AR_TX_FIXTURES=yes + +Or, by hand: + + AR_TX_FIXTURES=yes ruby -I connections/native_sqlite3 base_test.rb diff --git a/vendor/plugins/acts_as_versioned/Rakefile b/vendor/plugins/acts_as_versioned/Rakefile new file mode 100644 index 0000000..3ae69e9 --- /dev/null +++ b/vendor/plugins/acts_as_versioned/Rakefile @@ -0,0 +1,182 @@ +require 'rubygems' + +Gem::manage_gems + +require 'rake/rdoctask' +require 'rake/packagetask' +require 'rake/gempackagetask' +require 'rake/testtask' +require 'rake/contrib/rubyforgepublisher' + +PKG_NAME = 'acts_as_versioned' +PKG_VERSION = '0.3.1' +PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}" +PROD_HOST = "technoweenie@bidwell.textdrive.com" +RUBY_FORGE_PROJECT = 'ar-versioned' +RUBY_FORGE_USER = 'technoweenie' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the calculations plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the calculations plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = "#{PKG_NAME} -- Simple versioning with active record models" + rdoc.options << '--line-numbers --inline-source' + rdoc.rdoc_files.include('README', 'CHANGELOG', 'RUNNING_UNIT_TESTS') + rdoc.rdoc_files.include('lib/**/*.rb') +end + +spec = Gem::Specification.new do |s| + s.name = PKG_NAME + s.version = PKG_VERSION + s.platform = Gem::Platform::RUBY + s.summary = "Simple versioning with active record models" + s.files = FileList["{lib,test}/**/*"].to_a + %w(README MIT-LICENSE CHANGELOG RUNNING_UNIT_TESTS) + s.files.delete "acts_as_versioned_plugin.sqlite.db" + s.files.delete "acts_as_versioned_plugin.sqlite3.db" + s.files.delete "test/debug.log" + s.require_path = 'lib' + s.autorequire = 'acts_as_versioned' + s.has_rdoc = true + s.test_files = Dir['test/**/*_test.rb'] + s.add_dependency 'activerecord', '>= 1.10.1' + s.add_dependency 'activesupport', '>= 1.1.1' + s.author = "Rick Olson" + s.email = "technoweenie@gmail.com" + s.homepage = "http://techno-weenie.net" +end + +Rake::GemPackageTask.new(spec) do |pkg| + pkg.need_tar = true +end + +desc "Publish the API documentation" +task :pdoc => [:rdoc] do + Rake::RubyForgePublisher.new(RUBY_FORGE_PROJECT, RUBY_FORGE_USER).upload +end + +desc 'Publish the gem and API docs' +task :publish => [:pdoc, :rubyforge_upload] + +desc "Publish the release files to RubyForge." +task :rubyforge_upload => :package do + files = %w(gem tgz).map { |ext| "pkg/#{PKG_FILE_NAME}.#{ext}" } + + if RUBY_FORGE_PROJECT then + require 'net/http' + require 'open-uri' + + project_uri = "http://rubyforge.org/projects/#{RUBY_FORGE_PROJECT}/" + project_data = open(project_uri) { |data| data.read } + group_id = project_data[/[?&]group_id=(\d+)/, 1] + raise "Couldn't get group id" unless group_id + + # This echos password to shell which is a bit sucky + if ENV["RUBY_FORGE_PASSWORD"] + password = ENV["RUBY_FORGE_PASSWORD"] + else + print "#{RUBY_FORGE_USER}@rubyforge.org's password: " + password = STDIN.gets.chomp + end + + login_response = Net::HTTP.start("rubyforge.org", 80) do |http| + data = [ + "login=1", + "form_loginname=#{RUBY_FORGE_USER}", + "form_pw=#{password}" + ].join("&") + http.post("/account/login.php", data) + end + + cookie = login_response["set-cookie"] + raise "Login failed" unless cookie + headers = { "Cookie" => cookie } + + release_uri = "http://rubyforge.org/frs/admin/?group_id=#{group_id}" + release_data = open(release_uri, headers) { |data| data.read } + package_id = release_data[/[?&]package_id=(\d+)/, 1] + raise "Couldn't get package id" unless package_id + + first_file = true + release_id = "" + + files.each do |filename| + basename = File.basename(filename) + file_ext = File.extname(filename) + file_data = File.open(filename, "rb") { |file| file.read } + + puts "Releasing #{basename}..." + + release_response = Net::HTTP.start("rubyforge.org", 80) do |http| + release_date = Time.now.strftime("%Y-%m-%d %H:%M") + type_map = { + ".zip" => "3000", + ".tgz" => "3110", + ".gz" => "3110", + ".gem" => "1400" + }; type_map.default = "9999" + type = type_map[file_ext] + boundary = "rubyqMY6QN9bp6e4kS21H4y0zxcvoor" + + query_hash = if first_file then + { + "group_id" => group_id, + "package_id" => package_id, + "release_name" => PKG_FILE_NAME, + "release_date" => release_date, + "type_id" => type, + "processor_id" => "8000", # Any + "release_notes" => "", + "release_changes" => "", + "preformatted" => "1", + "submit" => "1" + } + else + { + "group_id" => group_id, + "release_id" => release_id, + "package_id" => package_id, + "step2" => "1", + "type_id" => type, + "processor_id" => "8000", # Any + "submit" => "Add This File" + } + end + + query = "?" + query_hash.map do |(name, value)| + [name, URI.encode(value)].join("=") + end.join("&") + + data = [ + "--" + boundary, + "Content-Disposition: form-data; name=\"userfile\"; filename=\"#{basename}\"", + "Content-Type: application/octet-stream", + "Content-Transfer-Encoding: binary", + "", file_data, "" + ].join("\x0D\x0A") + + release_headers = headers.merge( + "Content-Type" => "multipart/form-data; boundary=#{boundary}" + ) + + target = first_file ? "/frs/admin/qrs.php" : "/frs/admin/editrelease.php" + http.post(target + query, data, release_headers) + end + + if first_file then + release_id = release_response.body[/release_id=(\d+)/, 1] + raise("Couldn't get release id") unless release_id + end + + first_file = false + end + end +end \ No newline at end of file diff --git a/vendor/plugins/acts_as_versioned/init.rb b/vendor/plugins/acts_as_versioned/init.rb new file mode 100644 index 0000000..5937bbc --- /dev/null +++ b/vendor/plugins/acts_as_versioned/init.rb @@ -0,0 +1 @@ +require 'acts_as_versioned' \ No newline at end of file diff --git a/vendor/plugins/acts_as_versioned/lib/acts_as_versioned.rb b/vendor/plugins/acts_as_versioned/lib/acts_as_versioned.rb new file mode 100644 index 0000000..b69abbe --- /dev/null +++ b/vendor/plugins/acts_as_versioned/lib/acts_as_versioned.rb @@ -0,0 +1,545 @@ +# Copyright (c) 2005 Rick Olson +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +module ActiveRecord #:nodoc: + module Acts #:nodoc: + # Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a + # versioned table ready and that your model has a version field. This works with optimistic locking if the lock_version + # column is present as well. + # + # 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 + # your container for the changes to be reflected. In development mode this usually means restarting WEBrick. + # + # class Page < ActiveRecord::Base + # # assumes pages_versions table + # acts_as_versioned + # end + # + # Example: + # + # page = Page.create(:title => 'hello world!') + # page.version # => 1 + # + # page.title = 'hello world' + # page.save + # page.version # => 2 + # page.versions.size # => 2 + # + # page.revert_to(1) # using version number + # page.title # => 'hello world!' + # + # page.revert_to(page.versions.last) # using versioned instance + # page.title # => 'hello world' + # + # page.versions.earliest # efficient query to find the first version + # page.versions.latest # efficient query to find the most recently created version + # + # + # Simple Queries to page between versions + # + # page.versions.before(version) + # page.versions.after(version) + # + # Access the previous/next versions from the versioned model itself + # + # version = page.versions.latest + # version.previous # go back one version + # version.next # go forward one version + # + # See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options + module Versioned + CALLBACKS = [:set_new_version, :save_version_on_create, :save_version?, :clear_changed_attributes] + def self.included(base) # :nodoc: + base.extend ClassMethods + end + + module ClassMethods + # == Configuration options + # + # * class_name - versioned model class name (default: PageVersion in the above example) + # * table_name - versioned model table name (default: page_versions in the above example) + # * foreign_key - foreign key used to relate the versioned model to the original model (default: page_id in the above example) + # * inheritance_column - name of the column to save the model's inheritance_column value for STI. (default: versioned_type) + # * version_column - name of the column in the model that keeps the version number (default: version) + # * sequence_name - name of the custom sequence to be used by the versioned model. + # * limit - number of revisions to keep, defaults to unlimited + # * if - symbol of method to check before saving a new version. If this method returns false, a new version is not saved. + # For finer control, pass either a Proc or modify Model#version_condition_met? + # + # acts_as_versioned :if => Proc.new { |auction| !auction.expired? } + # + # or... + # + # class Auction + # def version_condition_met? # totally bypasses the :if option + # !expired? + # end + # end + # + # * if_changed - Simple way of specifying attributes that are required to be changed before saving a model. This takes + # either a symbol or array of symbols. WARNING - This will attempt to overwrite any attribute setters you may have. + # Use this instead if you want to write your own attribute setters (and ignore if_changed): + # + # def name=(new_name) + # write_changed_attribute :name, new_name + # end + # + # * extend - Lets you specify a module to be mixed in both the original and versioned models. You can also just pass a block + # to create an anonymous mixin: + # + # class Auction + # acts_as_versioned do + # def started? + # !started_at.nil? + # end + # end + # end + # + # or... + # + # module AuctionExtension + # def started? + # !started_at.nil? + # end + # end + # class Auction + # acts_as_versioned :extend => AuctionExtension + # end + # + # Example code: + # + # @auction = Auction.find(1) + # @auction.started? + # @auction.versions.first.started? + # + # == Database Schema + # + # The model that you're versioning needs to have a 'version' attribute. The model is versioned + # into a table called #{model}_versions where the model name is singlular. The _versions table should + # contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field. + # + # A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance, + # then that field is reflected in the versioned model as 'versioned_type' by default. + # + # Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table + # method, perfect for a migration. It will also create the version column if the main model does not already have it. + # + # class AddVersions < ActiveRecord::Migration + # def self.up + # # create_versioned_table takes the same options hash + # # that create_table does + # Post.create_versioned_table + # end + # + # def self.down + # Post.drop_versioned_table + # end + # end + # + # == Changing What Fields Are Versioned + # + # By default, acts_as_versioned will version all but these fields: + # + # [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column] + # + # You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols. + # + # class Post < ActiveRecord::Base + # acts_as_versioned + # self.non_versioned_columns << 'comments_count' + # end + # + def acts_as_versioned(options = {}, &extension) + # don't allow multiple calls + return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods) + + send :include, ActiveRecord::Acts::Versioned::ActMethods + + cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column, + :version_column, :max_version_limit, :track_changed_attributes, :version_condition, :version_sequence_name, :non_versioned_columns, + :version_association_options + + # legacy + alias_method :non_versioned_fields, :non_versioned_columns + alias_method :non_versioned_fields=, :non_versioned_columns= + + class << self + alias_method :non_versioned_fields, :non_versioned_columns + alias_method :non_versioned_fields=, :non_versioned_columns= + end + + send :attr_accessor, :changed_attributes + + self.versioned_class_name = options[:class_name] || "Version" + self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key + self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}" + self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}" + self.version_column = options[:version_column] || 'version' + self.version_sequence_name = options[:sequence_name] + self.max_version_limit = options[:limit].to_i + self.version_condition = options[:if] || true + self.non_versioned_columns = [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column] + self.version_association_options = { + :class_name => "#{self.to_s}::#{versioned_class_name}", + :foreign_key => versioned_foreign_key, + :order => 'version', + :dependent => :delete_all + }.merge(options[:association_options] || {}) + + if block_given? + extension_module_name = "#{versioned_class_name}Extension" + silence_warnings do + self.const_set(extension_module_name, Module.new(&extension)) + end + + options[:extend] = self.const_get(extension_module_name) + end + + class_eval do + has_many :versions, version_association_options do + # finds earliest version of this record + def earliest + @earliest ||= find(:first) + end + + # find latest version of this record + def latest + @latest ||= find(:first, :order => 'version desc') + end + end + before_save :set_new_version + after_create :save_version_on_create + after_update :save_version + after_save :clear_old_versions + after_save :clear_changed_attributes + + unless options[:if_changed].nil? + self.track_changed_attributes = true + options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array) + options[:if_changed].each do |attr_name| + define_method("#{attr_name}=") do |value| + write_changed_attribute attr_name, value + end + end + end + + include options[:extend] if options[:extend].is_a?(Module) + end + + # create the dynamic versioned model + const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do + def self.reloadable? ; false ; end + # find first version before the given version + def self.before(version) + find :first, :order => 'version desc', + :conditions => ["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version] + end + + # find first version after the given version. + def self.after(version) + find :first, :order => 'version', + :conditions => ["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version] + end + + def previous + self.class.before(self) + end + + def next + self.class.after(self) + end + end + + versioned_class.cattr_accessor :original_class + versioned_class.original_class = self + versioned_class.set_table_name versioned_table_name + versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym, + :class_name => "::#{self.to_s}", + :foreign_key => versioned_foreign_key + versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module) + versioned_class.set_sequence_name version_sequence_name if version_sequence_name + end + end + + module ActMethods + def self.included(base) # :nodoc: + base.extend ClassMethods + end + + # Saves a version of the model if applicable + def save_version + save_version_on_create if save_version? + end + + # Saves a version of the model in the versioned table. This is called in the after_save callback by default + def save_version_on_create + rev = self.class.versioned_class.new + self.clone_versioned_model(self, rev) + rev.version = send(self.class.version_column) + rev.send("#{self.class.versioned_foreign_key}=", self.id) + rev.save + end + + # Clears old revisions if a limit is set with the :limit option in acts_as_versioned. + # Override this method to set your own criteria for clearing old versions. + def clear_old_versions + return if self.class.max_version_limit == 0 + excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit + if excess_baggage > 0 + sql = "DELETE FROM #{self.class.versioned_table_name} WHERE version <= #{excess_baggage} AND #{self.class.versioned_foreign_key} = #{self.id}" + self.class.versioned_class.connection.execute sql + end + end + + # Reverts a model to a given version. Takes either a version number or an instance of the versioned model + def revert_to(version) + if version.is_a?(self.class.versioned_class) + return false unless version.send(self.class.versioned_foreign_key) == self.id and !version.new_record? + else + return false unless version = versions.find_by_version(version) + end + self.clone_versioned_model(version, self) + self.send("#{self.class.version_column}=", version.version) + true + end + + # Reverts a model to a given version and saves the model. + # Takes either a version number or an instance of the versioned model + def revert_to!(version) + revert_to(version) ? save_without_revision : false + end + + # Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created. + def save_without_revision + save_without_revision! + true + rescue + false + end + + def save_without_revision! + without_locking do + without_revision do + save! + end + end + end + + # Returns an array of attribute keys that are versioned. See non_versioned_columns + def versioned_attributes + self.attributes.keys.select { |k| !self.class.non_versioned_columns.include?(k) } + end + + # If called with no parameters, gets whether the current model has changed and needs to be versioned. + # If called with a single parameter, gets whether the parameter has changed. + def changed?(attr_name = nil) + attr_name.nil? ? + (!self.class.track_changed_attributes || (changed_attributes && changed_attributes.length > 0)) : + (changed_attributes && changed_attributes.include?(attr_name.to_s)) + end + + # keep old dirty? method + alias_method :dirty?, :changed? + + # Clones a model. Used when saving a new version or reverting a model's version. + def clone_versioned_model(orig_model, new_model) + self.versioned_attributes.each do |key| + new_model.send("#{key}=", orig_model.send(key)) if orig_model.has_attribute?(key) + end + + if orig_model.is_a?(self.class.versioned_class) + new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column] + elsif new_model.is_a?(self.class.versioned_class) + new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column] + end + end + + # Checks whether a new version shall be saved or not. Calls version_condition_met? and changed?. + def save_version? + version_condition_met? && changed? + end + + # Checks condition set in the :if option to check whether a revision should be created or not. Override this for + # custom version condition checking. + def version_condition_met? + case + when version_condition.is_a?(Symbol) + send(version_condition) + when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1) + version_condition.call(self) + else + version_condition + end + end + + # Executes the block with the versioning callbacks disabled. + # + # @foo.without_revision do + # @foo.save + # end + # + def without_revision(&block) + self.class.without_revision(&block) + end + + # Turns off optimistic locking for the duration of the block + # + # @foo.without_locking do + # @foo.save + # end + # + def without_locking(&block) + self.class.without_locking(&block) + end + + def empty_callback() end #:nodoc: + + protected + # sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version. + def set_new_version + self.send("#{self.class.version_column}=", self.next_version) if new_record? || (!locking_enabled? && save_version?) + end + + # Gets the next available version for the current record, or 1 for a new record + def next_version + return 1 if new_record? + (versions.calculate(:max, :version) || 0) + 1 + end + + # clears current changed attributes. Called after save. + def clear_changed_attributes + self.changed_attributes = [] + end + + def write_changed_attribute(attr_name, attr_value) + # Convert to db type for comparison. Avoids failing Float<=>String comparisons. + attr_value_for_db = self.class.columns_hash[attr_name.to_s].type_cast(attr_value) + (self.changed_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) || self.send(attr_name) == attr_value_for_db + write_attribute(attr_name, attr_value_for_db) + end + + module ClassMethods + # Finds a specific version of a specific row of this model + def find_version(id, version) + find_versions(id, + :conditions => ["#{versioned_foreign_key} = ? AND version = ?", id, version], + :limit => 1).first + end + + # Finds versions of a specific model. Takes an options hash like find + def find_versions(id, options = {}) + versioned_class.find :all, { + :conditions => ["#{versioned_foreign_key} = ?", id], + :order => 'version' }.merge(options) + end + + # Returns an array of columns that are versioned. See non_versioned_columns + def versioned_columns + self.columns.select { |c| !non_versioned_columns.include?(c.name) } + end + + # Returns an instance of the dynamic versioned model + def versioned_class + const_get versioned_class_name + end + + # Rake migration task to create the versioned table using options passed to acts_as_versioned + def create_versioned_table(create_table_options = {}) + # create version column in main table if it does not exist + if !self.content_columns.find { |c| %w(version lock_version).include? c.name } + self.connection.add_column table_name, :version, :integer + end + + self.connection.create_table(versioned_table_name, create_table_options) do |t| + t.column versioned_foreign_key, :integer + t.column :version, :integer + end + + updated_col = nil + self.versioned_columns.each do |col| + updated_col = col if !updated_col && %(updated_at updated_on).include?(col.name) + self.connection.add_column versioned_table_name, col.name, col.type, + :limit => col.limit, + :default => col.default, + :scale => col.scale, + :precision => col.precision + end + + if type_col = self.columns_hash[inheritance_column] + self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type, + :limit => type_col.limit, + :default => type_col.default, + :scale => type_col.scale, + :precision => type_col.precision + end + + if updated_col.nil? + self.connection.add_column versioned_table_name, :updated_at, :timestamp + end + end + + # Rake migration task to drop the versioned table + def drop_versioned_table + self.connection.drop_table versioned_table_name + end + + # Executes the block with the versioning callbacks disabled. + # + # Foo.without_revision do + # @foo.save + # end + # + def without_revision(&block) + class_eval do + CALLBACKS.each do |attr_name| + alias_method "orig_#{attr_name}".to_sym, attr_name + alias_method attr_name, :empty_callback + end + end + block.call + ensure + class_eval do + CALLBACKS.each do |attr_name| + alias_method attr_name, "orig_#{attr_name}".to_sym + end + end + end + + # Turns off optimistic locking for the duration of the block + # + # Foo.without_locking do + # @foo.save + # end + # + def without_locking(&block) + current = ActiveRecord::Base.lock_optimistically + ActiveRecord::Base.lock_optimistically = false if current + result = block.call + ActiveRecord::Base.lock_optimistically = true if current + result + end + end + end + end + end +end + +ActiveRecord::Base.send :include, ActiveRecord::Acts::Versioned \ No newline at end of file diff --git a/vendor/plugins/acts_as_versioned/test/abstract_unit.rb b/vendor/plugins/acts_as_versioned/test/abstract_unit.rb new file mode 100644 index 0000000..86df5e1 --- /dev/null +++ b/vendor/plugins/acts_as_versioned/test/abstract_unit.rb @@ -0,0 +1,41 @@ +$:.unshift(File.dirname(__FILE__) + '/../../../rails/activesupport/lib') +$:.unshift(File.dirname(__FILE__) + '/../../../rails/activerecord/lib') +$:.unshift(File.dirname(__FILE__) + '/../lib') +require 'test/unit' +begin + require 'active_support' + require 'active_record' + require 'active_record/fixtures' +rescue LoadError + require 'rubygems' + retry +end +require 'acts_as_versioned' + +config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml')) +ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log") +ActiveRecord::Base.configurations = {'test' => config[ENV['DB'] || 'sqlite']} +ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) + +load(File.dirname(__FILE__) + "/schema.rb") + +# set up custom sequence on widget_versions for DBs that support sequences +if ENV['DB'] == 'postgresql' + ActiveRecord::Base.connection.execute "DROP SEQUENCE widgets_seq;" rescue nil + ActiveRecord::Base.connection.remove_column :widget_versions, :id + ActiveRecord::Base.connection.execute "CREATE SEQUENCE widgets_seq START 101;" + ActiveRecord::Base.connection.execute "ALTER TABLE widget_versions ADD COLUMN id INTEGER PRIMARY KEY DEFAULT nextval('widgets_seq');" +end + +Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/" +$:.unshift(Test::Unit::TestCase.fixture_path) + +class Test::Unit::TestCase #:nodoc: + # Turn off transactional fixtures if you're working with MyISAM tables in MySQL + self.use_transactional_fixtures = true + + # Instantiated fixtures are slow, but give you @david where you otherwise would need people(:david) + self.use_instantiated_fixtures = false + + # Add more helper methods to be used by all tests here... +end \ No newline at end of file diff --git a/vendor/plugins/acts_as_versioned/test/database.yml b/vendor/plugins/acts_as_versioned/test/database.yml new file mode 100644 index 0000000..506e6bd --- /dev/null +++ b/vendor/plugins/acts_as_versioned/test/database.yml @@ -0,0 +1,18 @@ +sqlite: + :adapter: sqlite + :dbfile: acts_as_versioned_plugin.sqlite.db +sqlite3: + :adapter: sqlite3 + :dbfile: acts_as_versioned_plugin.sqlite3.db +postgresql: + :adapter: postgresql + :username: postgres + :password: postgres + :database: acts_as_versioned_plugin_test + :min_messages: ERROR +mysql: + :adapter: mysql + :host: localhost + :username: rails + :password: + :database: acts_as_versioned_plugin_test \ No newline at end of file diff --git a/vendor/plugins/acts_as_versioned/test/fixtures/authors.yml b/vendor/plugins/acts_as_versioned/test/fixtures/authors.yml new file mode 100644 index 0000000..bd7a5ae --- /dev/null +++ b/vendor/plugins/acts_as_versioned/test/fixtures/authors.yml @@ -0,0 +1,6 @@ +caged: + id: 1 + name: caged +mly: + id: 2 + name: mly \ No newline at end of file diff --git a/vendor/plugins/acts_as_versioned/test/fixtures/landmark.rb b/vendor/plugins/acts_as_versioned/test/fixtures/landmark.rb new file mode 100644 index 0000000..cb9b930 --- /dev/null +++ b/vendor/plugins/acts_as_versioned/test/fixtures/landmark.rb @@ -0,0 +1,3 @@ +class Landmark < ActiveRecord::Base + acts_as_versioned :if_changed => [ :name, :longitude, :latitude ] +end diff --git a/vendor/plugins/acts_as_versioned/test/fixtures/landmark_versions.yml b/vendor/plugins/acts_as_versioned/test/fixtures/landmark_versions.yml new file mode 100644 index 0000000..2dbd54e --- /dev/null +++ b/vendor/plugins/acts_as_versioned/test/fixtures/landmark_versions.yml @@ -0,0 +1,7 @@ +washington: + id: 1 + landmark_id: 1 + version: 1 + name: Washington, D.C. + latitude: 38.895 + longitude: -77.036667 diff --git a/vendor/plugins/acts_as_versioned/test/fixtures/landmarks.yml b/vendor/plugins/acts_as_versioned/test/fixtures/landmarks.yml new file mode 100644 index 0000000..46d9617 --- /dev/null +++ b/vendor/plugins/acts_as_versioned/test/fixtures/landmarks.yml @@ -0,0 +1,6 @@ +washington: + id: 1 + name: Washington, D.C. + latitude: 38.895 + longitude: -77.036667 + version: 1 diff --git a/vendor/plugins/acts_as_versioned/test/fixtures/locked_pages.yml b/vendor/plugins/acts_as_versioned/test/fixtures/locked_pages.yml new file mode 100644 index 0000000..318e776 --- /dev/null +++ b/vendor/plugins/acts_as_versioned/test/fixtures/locked_pages.yml @@ -0,0 +1,10 @@ +welcome: + id: 1 + title: Welcome to the weblog + lock_version: 24 + type: LockedPage +thinking: + id: 2 + title: So I was thinking + lock_version: 24 + type: SpecialLockedPage diff --git a/vendor/plugins/acts_as_versioned/test/fixtures/locked_pages_revisions.yml b/vendor/plugins/acts_as_versioned/test/fixtures/locked_pages_revisions.yml new file mode 100644 index 0000000..5c978e6 --- /dev/null +++ b/vendor/plugins/acts_as_versioned/test/fixtures/locked_pages_revisions.yml @@ -0,0 +1,27 @@ +welcome_1: + id: 1 + page_id: 1 + title: Welcome to the weblg + version: 23 + version_type: LockedPage + +welcome_2: + id: 2 + page_id: 1 + title: Welcome to the weblog + version: 24 + version_type: LockedPage + +thinking_1: + id: 3 + page_id: 2 + title: So I was thinking!!! + version: 23 + version_type: SpecialLockedPage + +thinking_2: + id: 4 + page_id: 2 + title: So I was thinking + version: 24 + version_type: SpecialLockedPage diff --git a/vendor/plugins/acts_as_versioned/test/fixtures/migrations/1_add_versioned_tables.rb b/vendor/plugins/acts_as_versioned/test/fixtures/migrations/1_add_versioned_tables.rb new file mode 100644 index 0000000..5007b16 --- /dev/null +++ b/vendor/plugins/acts_as_versioned/test/fixtures/migrations/1_add_versioned_tables.rb @@ -0,0 +1,15 @@ +class AddVersionedTables < ActiveRecord::Migration + def self.up + create_table("things") do |t| + t.column :title, :text + t.column :price, :decimal, :precision => 7, :scale => 2 + t.column :type, :string + end + Thing.create_versioned_table + end + + def self.down + Thing.drop_versioned_table + drop_table "things" rescue nil + end +end \ No newline at end of file diff --git a/vendor/plugins/acts_as_versioned/test/fixtures/page.rb b/vendor/plugins/acts_as_versioned/test/fixtures/page.rb new file mode 100644 index 0000000..f133e35 --- /dev/null +++ b/vendor/plugins/acts_as_versioned/test/fixtures/page.rb @@ -0,0 +1,43 @@ +class Page < ActiveRecord::Base + belongs_to :author + has_many :authors, :through => :versions, :order => 'name' + belongs_to :revisor, :class_name => 'Author' + has_many :revisors, :class_name => 'Author', :through => :versions, :order => 'name' + acts_as_versioned :if => :feeling_good? do + def self.included(base) + base.cattr_accessor :feeling_good + base.feeling_good = true + base.belongs_to :author + base.belongs_to :revisor, :class_name => 'Author' + end + + def feeling_good? + @@feeling_good == true + end + end +end + +module LockedPageExtension + def hello_world + 'hello_world' + end +end + +class LockedPage < ActiveRecord::Base + acts_as_versioned \ + :inheritance_column => :version_type, + :foreign_key => :page_id, + :table_name => :locked_pages_revisions, + :class_name => 'LockedPageRevision', + :version_column => :lock_version, + :limit => 2, + :if_changed => :title, + :extend => LockedPageExtension +end + +class SpecialLockedPage < LockedPage +end + +class Author < ActiveRecord::Base + has_many :pages +end \ No newline at end of file diff --git a/vendor/plugins/acts_as_versioned/test/fixtures/page_versions.yml b/vendor/plugins/acts_as_versioned/test/fixtures/page_versions.yml new file mode 100644 index 0000000..ef565fa --- /dev/null +++ b/vendor/plugins/acts_as_versioned/test/fixtures/page_versions.yml @@ -0,0 +1,16 @@ +welcome_2: + id: 1 + page_id: 1 + title: Welcome to the weblog + body: Such a lovely day + version: 24 + author_id: 1 + revisor_id: 1 +welcome_1: + id: 2 + page_id: 1 + title: Welcome to the weblg + body: Such a lovely day + version: 23 + author_id: 2 + revisor_id: 2 diff --git a/vendor/plugins/acts_as_versioned/test/fixtures/pages.yml b/vendor/plugins/acts_as_versioned/test/fixtures/pages.yml new file mode 100644 index 0000000..07ac51f --- /dev/null +++ b/vendor/plugins/acts_as_versioned/test/fixtures/pages.yml @@ -0,0 +1,7 @@ +welcome: + id: 1 + title: Welcome to the weblog + body: Such a lovely day + version: 24 + author_id: 1 + revisor_id: 1 \ No newline at end of file diff --git a/vendor/plugins/acts_as_versioned/test/fixtures/widget.rb b/vendor/plugins/acts_as_versioned/test/fixtures/widget.rb new file mode 100644 index 0000000..086ac2b --- /dev/null +++ b/vendor/plugins/acts_as_versioned/test/fixtures/widget.rb @@ -0,0 +1,6 @@ +class Widget < ActiveRecord::Base + acts_as_versioned :sequence_name => 'widgets_seq', :association_options => { + :dependent => :nullify, :order => 'version desc' + } + non_versioned_columns << 'foo' +end \ No newline at end of file diff --git a/vendor/plugins/acts_as_versioned/test/migration_test.rb b/vendor/plugins/acts_as_versioned/test/migration_test.rb new file mode 100644 index 0000000..3cef741 --- /dev/null +++ b/vendor/plugins/acts_as_versioned/test/migration_test.rb @@ -0,0 +1,41 @@ +require File.join(File.dirname(__FILE__), 'abstract_unit') + +if ActiveRecord::Base.connection.supports_migrations? + class Thing < ActiveRecord::Base + attr_accessor :version + acts_as_versioned + end + + class MigrationTest < Test::Unit::TestCase + self.use_transactional_fixtures = false + def teardown + ActiveRecord::Base.connection.initialize_schema_information + ActiveRecord::Base.connection.update "UPDATE schema_info SET version = 0" + + Thing.connection.drop_table "things" rescue nil + Thing.connection.drop_table "thing_versions" rescue nil + Thing.reset_column_information + end + + def test_versioned_migration + assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' } + # take 'er up + ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/') + t = Thing.create :title => 'blah blah', :price => 123.45, :type => 'Thing' + assert_equal 1, t.versions.size + + # check that the price column has remembered its value correctly + assert_equal t.price, t.versions.first.price + assert_equal t.title, t.versions.first.title + assert_equal t[:type], t.versions.first[:type] + + # make sure that the precision of the price column has been preserved + assert_equal 7, Thing::Version.columns.find{|c| c.name == "price"}.precision + assert_equal 2, Thing::Version.columns.find{|c| c.name == "price"}.scale + + # now lets take 'er back down + ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/') + assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' } + end + end +end diff --git a/vendor/plugins/acts_as_versioned/test/schema.rb b/vendor/plugins/acts_as_versioned/test/schema.rb new file mode 100644 index 0000000..7d5153d --- /dev/null +++ b/vendor/plugins/acts_as_versioned/test/schema.rb @@ -0,0 +1,68 @@ +ActiveRecord::Schema.define(:version => 0) do + create_table :pages, :force => true do |t| + t.column :version, :integer + t.column :title, :string, :limit => 255 + t.column :body, :text + t.column :updated_on, :datetime + t.column :author_id, :integer + t.column :revisor_id, :integer + end + + create_table :page_versions, :force => true do |t| + t.column :page_id, :integer + t.column :version, :integer + t.column :title, :string, :limit => 255 + t.column :body, :text + t.column :updated_on, :datetime + t.column :author_id, :integer + t.column :revisor_id, :integer + end + + create_table :authors, :force => true do |t| + t.column :page_id, :integer + t.column :name, :string + end + + create_table :locked_pages, :force => true do |t| + t.column :lock_version, :integer + t.column :title, :string, :limit => 255 + t.column :type, :string, :limit => 255 + end + + create_table :locked_pages_revisions, :force => true do |t| + t.column :page_id, :integer + t.column :version, :integer + t.column :title, :string, :limit => 255 + t.column :version_type, :string, :limit => 255 + t.column :updated_at, :datetime + end + + create_table :widgets, :force => true do |t| + t.column :name, :string, :limit => 50 + t.column :foo, :string + t.column :version, :integer + t.column :updated_at, :datetime + end + + create_table :widget_versions, :force => true do |t| + t.column :widget_id, :integer + t.column :name, :string, :limit => 50 + t.column :version, :integer + t.column :updated_at, :datetime + end + + create_table :landmarks, :force => true do |t| + t.column :name, :string + t.column :latitude, :float + t.column :longitude, :float + t.column :version, :integer + end + + create_table :landmark_versions, :force => true do |t| + t.column :landmark_id, :integer + t.column :name, :string + t.column :latitude, :float + t.column :longitude, :float + t.column :version, :integer + end +end diff --git a/vendor/plugins/acts_as_versioned/test/versioned_test.rb b/vendor/plugins/acts_as_versioned/test/versioned_test.rb new file mode 100644 index 0000000..1e22a69 --- /dev/null +++ b/vendor/plugins/acts_as_versioned/test/versioned_test.rb @@ -0,0 +1,328 @@ +require File.join(File.dirname(__FILE__), 'abstract_unit') +require File.join(File.dirname(__FILE__), 'fixtures/page') +require File.join(File.dirname(__FILE__), 'fixtures/widget') + +class VersionedTest < Test::Unit::TestCase + fixtures :pages, :page_versions, :locked_pages, :locked_pages_revisions, :authors, :landmarks, :landmark_versions + set_fixture_class :page_versions => Page::Version + + def test_saves_versioned_copy + p = Page.create! :title => 'first title', :body => 'first body' + assert !p.new_record? + assert_equal 1, p.versions.size + assert_equal 1, p.version + assert_instance_of Page.versioned_class, p.versions.first + end + + def test_saves_without_revision + p = pages(:welcome) + old_versions = p.versions.count + + p.save_without_revision + + p.without_revision do + p.update_attributes :title => 'changed' + end + + assert_equal old_versions, p.versions.count + end + + def test_rollback_with_version_number + p = pages(:welcome) + assert_equal 24, p.version + assert_equal 'Welcome to the weblog', p.title + + assert p.revert_to!(p.versions.first.version), "Couldn't revert to 23" + assert_equal 23, p.version + assert_equal 'Welcome to the weblg', p.title + end + + def test_versioned_class_name + assert_equal 'Version', Page.versioned_class_name + assert_equal 'LockedPageRevision', LockedPage.versioned_class_name + end + + def test_versioned_class + assert_equal Page::Version, Page.versioned_class + assert_equal LockedPage::LockedPageRevision, LockedPage.versioned_class + end + + def test_special_methods + assert_nothing_raised { pages(:welcome).feeling_good? } + assert_nothing_raised { pages(:welcome).versions.first.feeling_good? } + assert_nothing_raised { locked_pages(:welcome).hello_world } + assert_nothing_raised { locked_pages(:welcome).versions.first.hello_world } + end + + def test_rollback_with_version_class + p = pages(:welcome) + assert_equal 24, p.version + assert_equal 'Welcome to the weblog', p.title + + assert p.revert_to!(p.versions.first), "Couldn't revert to 23" + assert_equal 23, p.version + assert_equal 'Welcome to the weblg', p.title + end + + def test_rollback_fails_with_invalid_revision + p = locked_pages(:welcome) + assert !p.revert_to!(locked_pages(:thinking)) + end + + def test_saves_versioned_copy_with_options + p = LockedPage.create! :title => 'first title' + assert !p.new_record? + assert_equal 1, p.versions.size + assert_instance_of LockedPage.versioned_class, p.versions.first + end + + def test_rollback_with_version_number_with_options + p = locked_pages(:welcome) + assert_equal 'Welcome to the weblog', p.title + assert_equal 'LockedPage', p.versions.first.version_type + + assert p.revert_to!(p.versions.first.version), "Couldn't revert to 23" + assert_equal 'Welcome to the weblg', p.title + assert_equal 'LockedPage', p.versions.first.version_type + end + + def test_rollback_with_version_class_with_options + p = locked_pages(:welcome) + assert_equal 'Welcome to the weblog', p.title + assert_equal 'LockedPage', p.versions.first.version_type + + assert p.revert_to!(p.versions.first), "Couldn't revert to 1" + assert_equal 'Welcome to the weblg', p.title + assert_equal 'LockedPage', p.versions.first.version_type + end + + def test_saves_versioned_copy_with_sti + p = SpecialLockedPage.create! :title => 'first title' + assert !p.new_record? + assert_equal 1, p.versions.size + assert_instance_of LockedPage.versioned_class, p.versions.first + assert_equal 'SpecialLockedPage', p.versions.first.version_type + end + + def test_rollback_with_version_number_with_sti + p = locked_pages(:thinking) + assert_equal 'So I was thinking', p.title + + assert p.revert_to!(p.versions.first.version), "Couldn't revert to 1" + assert_equal 'So I was thinking!!!', p.title + assert_equal 'SpecialLockedPage', p.versions.first.version_type + end + + def test_lock_version_works_with_versioning + p = locked_pages(:thinking) + p2 = LockedPage.find(p.id) + + p.title = 'fresh title' + p.save + assert_equal 2, p.versions.size # limit! + + assert_raises(ActiveRecord::StaleObjectError) do + p2.title = 'stale title' + p2.save + end + end + + def test_version_if_condition + p = Page.create! :title => "title" + assert_equal 1, p.version + + Page.feeling_good = false + p.save + assert_equal 1, p.version + Page.feeling_good = true + end + + def test_version_if_condition2 + # set new if condition + Page.class_eval do + def new_feeling_good() title[0..0] == 'a'; end + alias_method :old_feeling_good, :feeling_good? + alias_method :feeling_good?, :new_feeling_good + end + + p = Page.create! :title => "title" + assert_equal 1, p.version # version does not increment + assert_equal 1, p.versions(true).size + + p.update_attributes(:title => 'new title') + assert_equal 1, p.version # version does not increment + assert_equal 1, p.versions(true).size + + p.update_attributes(:title => 'a title') + assert_equal 2, p.version + assert_equal 2, p.versions(true).size + + # reset original if condition + Page.class_eval { alias_method :feeling_good?, :old_feeling_good } + end + + def test_version_if_condition_with_block + # set new if condition + old_condition = Page.version_condition + Page.version_condition = Proc.new { |page| page.title[0..0] == 'b' } + + p = Page.create! :title => "title" + assert_equal 1, p.version # version does not increment + assert_equal 1, p.versions(true).size + + p.update_attributes(:title => 'a title') + assert_equal 1, p.version # version does not increment + assert_equal 1, p.versions(true).size + + p.update_attributes(:title => 'b title') + assert_equal 2, p.version + assert_equal 2, p.versions(true).size + + # reset original if condition + Page.version_condition = old_condition + end + + def test_version_no_limit + p = Page.create! :title => "title", :body => 'first body' + p.save + p.save + 5.times do |i| + assert_page_title p, i + end + end + + def test_version_max_limit + p = LockedPage.create! :title => "title" + p.update_attributes(:title => "title1") + p.update_attributes(:title => "title2") + 5.times do |i| + assert_page_title p, i, :lock_version + assert p.versions(true).size <= 2, "locked version can only store 2 versions" + end + end + + def test_track_changed_attributes_default_value + assert !Page.track_changed_attributes + assert LockedPage.track_changed_attributes + assert SpecialLockedPage.track_changed_attributes + end + + def test_version_order + assert_equal 23, pages(:welcome).versions.first.version + assert_equal 24, pages(:welcome).versions.last.version + end + + def test_track_changed_attributes + p = LockedPage.create! :title => "title" + assert_equal 1, p.lock_version + assert_equal 1, p.versions(true).size + + p.title = 'title' + assert !p.save_version? + p.save + assert_equal 2, p.lock_version # still increments version because of optimistic locking + assert_equal 1, p.versions(true).size + + p.title = 'updated title' + assert p.save_version? + p.save + assert_equal 3, p.lock_version + assert_equal 1, p.versions(true).size # version 1 deleted + + p.title = 'updated title!' + assert p.save_version? + p.save + assert_equal 4, p.lock_version + assert_equal 2, p.versions(true).size # version 1 deleted + end + + def assert_page_title(p, i, version_field = :version) + p.title = "title#{i}" + p.save + assert_equal "title#{i}", p.title + assert_equal (i+4), p.send(version_field) + end + + def test_find_versions + assert_equal 2, locked_pages(:welcome).versions.size + assert_equal 1, locked_pages(:welcome).versions.find(:all, :conditions => ['title LIKE ?', '%weblog%']).length + assert_equal 2, locked_pages(:welcome).versions.find(:all, :conditions => ['title LIKE ?', '%web%']).length + assert_equal 0, locked_pages(:thinking).versions.find(:all, :conditions => ['title LIKE ?', '%web%']).length + assert_equal 2, locked_pages(:welcome).versions.length + end + + def test_with_sequence + assert_equal 'widgets_seq', Widget.versioned_class.sequence_name + 3.times { Widget.create! :name => 'new widget' } + assert_equal 3, Widget.count + assert_equal 3, Widget.versioned_class.count + end + + def test_has_many_through + assert_equal [authors(:caged), authors(:mly)], pages(:welcome).authors + end + + def test_has_many_through_with_custom_association + assert_equal [authors(:caged), authors(:mly)], pages(:welcome).revisors + end + + def test_referential_integrity + pages(:welcome).destroy + assert_equal 0, Page.count + assert_equal 0, Page::Version.count + end + + def test_association_options + association = Page.reflect_on_association(:versions) + options = association.options + assert_equal :delete_all, options[:dependent] + assert_equal 'version', options[:order] + + association = Widget.reflect_on_association(:versions) + options = association.options + assert_equal :nullify, options[:dependent] + assert_equal 'version desc', options[:order] + assert_equal 'widget_id', options[:foreign_key] + + widget = Widget.create! :name => 'new widget' + assert_equal 1, Widget.count + assert_equal 1, Widget.versioned_class.count + widget.destroy + assert_equal 0, Widget.count + assert_equal 1, Widget.versioned_class.count + end + + def test_versioned_records_should_belong_to_parent + page = pages(:welcome) + page_version = page.versions.last + assert_equal page, page_version.page + end + + def test_unchanged_attributes + landmarks(:washington).attributes = landmarks(:washington).attributes.except("id") + assert !landmarks(:washington).changed? + end + + def test_unchanged_string_attributes + landmarks(:washington).attributes = landmarks(:washington).attributes.except("id").inject({}) { |params, (key, value)| params.update(key => value.to_s) } + assert !landmarks(:washington).changed? + end + + def test_should_find_earliest_version + assert_equal page_versions(:welcome_1), pages(:welcome).versions.earliest + end + + def test_should_find_latest_version + assert_equal page_versions(:welcome_2), pages(:welcome).versions.latest + end + + def test_should_find_previous_version + assert_equal page_versions(:welcome_1), page_versions(:welcome_2).previous + assert_equal page_versions(:welcome_1), pages(:welcome).versions.before(page_versions(:welcome_2)) + end + + def test_should_find_next_version + assert_equal page_versions(:welcome_2), page_versions(:welcome_1).next + assert_equal page_versions(:welcome_2), pages(:welcome).versions.after(page_versions(:welcome_1)) + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/CHANGELOG b/vendor/plugins/attachment_fu/CHANGELOG new file mode 100644 index 0000000..3dd22dd --- /dev/null +++ b/vendor/plugins/attachment_fu/CHANGELOG @@ -0,0 +1,19 @@ +* April 2, 2007 * + +* don't copy the #full_filename to the default #temp_paths array if it doesn't exist +* add default ID partitioning for attachments +* add #binmode call to Tempfile (note: ruby should be doing this!) [Eric Beland] +* Check for current type of :thumbnails option. +* allow customization of the S3 configuration file path with the :s3_config_path option. +* Don't try to remove thumbnails if there aren't any. Closes #3 [ben stiglitz] + +* BC * (before changelog) + +* add default #temp_paths entry [mattly] +* add MiniMagick support to attachment_fu [Isacc] +* update #destroy_file to clear out any empty directories too [carlivar] +* fix references to S3Backend module [Hunter Hillegas] +* make #current_data public with db_file and s3 backends [ebryn] +* oops, actually svn add the files for s3 backend. [Jeffrey Hardy] +* experimental s3 support, egad, no tests.... [Jeffrey Hardy] +* doh, fix a few bad references to ActsAsAttachment [sixty4bit] \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/README b/vendor/plugins/attachment_fu/README new file mode 100644 index 0000000..d9da4ef --- /dev/null +++ b/vendor/plugins/attachment_fu/README @@ -0,0 +1,162 @@ +attachment-fu +===================== + +attachment_fu is a plugin by Rick Olson (aka technoweenie ) and is the successor to acts_as_attachment. To get a basic run-through of its capabilities, check out Mike Clark's tutorial . + + +attachment_fu functionality +=========================== + +attachment_fu facilitates file uploads in Ruby on Rails. There are a few storage options for the actual file data, but the plugin always at a minimum stores metadata for each file in the database. + +There are three storage options for files uploaded through attachment_fu: + File system + Database file + Amazon S3 + +Each method of storage many options associated with it that will be covered in the following section. Something to note, however, is that the Amazon S3 storage requires you to modify config/amazon_s3.yml and the Database file storage requires an extra table. + + +attachment_fu models +==================== + +For all three of these storage options a table of metadata is required. This table will contain information about the file (hence the 'meta') and its location. This table has no restrictions on naming, unlike the extra table required for database storage, which must have a table name of db_files (and by convention a model of DbFile). + +In the model there are two methods made available by this plugins: has_attachment and validates_as_attachment. + +has_attachment(options = {}) + This method accepts the options in a hash: + :content_type # Allowed content types. + # Allows all by default. Use :image to allow all standard image types. + :min_size # Minimum size allowed. + # 1 byte is the default. + :max_size # Maximum size allowed. + # 1.megabyte is the default. + :size # Range of sizes allowed. + # (1..1.megabyte) is the default. This overrides the :min_size and :max_size options. + :resize_to # Used by RMagick to resize images. + # Pass either an array of width/height, or a geometry string. + :thumbnails # Specifies a set of thumbnails to generate. + # This accepts a hash of filename suffixes and RMagick resizing options. + # This option need only be included if you want thumbnailing. + :thumbnail_class # Set which model class to use for thumbnails. + # This current attachment class is used by default. + :path_prefix # path to store the uploaded files. + # Uses public/#{table_name} by default for the filesystem, and just #{table_name} for the S3 backend. + # Setting this sets the :storage to :file_system. + :storage # Specifies the storage system to use.. + # Defaults to :db_file. Options are :file_system, :db_file, and :s3. + :processor # Sets the image processor to use for resizing of the attached image. + # Options include ImageScience, Rmagick, and MiniMagick. Default is whatever is installed. + + + Examples: + has_attachment :max_size => 1.kilobyte + has_attachment :size => 1.megabyte..2.megabytes + has_attachment :content_type => 'application/pdf' + has_attachment :content_type => ['application/pdf', 'application/msword', 'text/plain'] + has_attachment :content_type => :image, :resize_to => [50,50] + has_attachment :content_type => ['application/pdf', :image], :resize_to => 'x50' + has_attachment :thumbnails => { :thumb => [50, 50], :geometry => 'x50' } + has_attachment :storage => :file_system, :path_prefix => 'public/files' + has_attachment :storage => :file_system, :path_prefix => 'public/files', + :content_type => :image, :resize_to => [50,50] + has_attachment :storage => :file_system, :path_prefix => 'public/files', + :thumbnails => { :thumb => [50, 50], :geometry => 'x50' } + has_attachment :storage => :s3 + +validates_as_attachment + This method prevents files outside of the valid range (:min_size to :max_size, or the :size range) from being saved. It does not however, halt the upload of such files. They will be uploaded into memory regardless of size before validation. + + Example: + validates_as_attachment + + +attachment_fu migrations +======================== + +Fields for attachment_fu metadata tables... + in general: + size, :integer # file size in bytes + content_type, :string # mime type, ex: application/mp3 + filename, :string # sanitized filename + that reference images: + height, :integer # in pixels + width, :integer # in pixels + that reference images that will be thumbnailed: + parent_id, :integer # id of parent image (on the same table, a self-referencing foreign-key). + # Only populated if the current object is a thumbnail. + thumbnail, :string # the 'type' of thumbnail this attachment record describes. + # Only populated if the current object is a thumbnail. + # Usage: + # [ In Model 'Avatar' ] + # has_attachment :content_type => :image, + # :storage => :file_system, + # :max_size => 500.kilobytes, + # :resize_to => '320x200>', + # :thumbnails => { :small => '10x10>', + # :thumb => '100x100>' } + # [ Elsewhere ] + # @user.avatar.thumbnails.first.thumbnail #=> 'small' + that reference files stored in the database (:db_file): + db_file_id, :integer # id of the file in the database (foreign key) + +Field for attachment_fu db_files table: + data, :binary # binary file data, for use in database file storage + + +attachment_fu views +=================== + +There are two main views tasks that will be directly affected by attachment_fu: upload forms and displaying uploaded images. + +There are two parts of the upload form that differ from typical usage. + 1. Include ':multipart => true' in the html options of the form_for tag. + Example: + <% form_for(:attachment_metadata, :url => { :action => "create" }, :html => { :multipart => true }) do |form| %> + + 2. Use the file_field helper with :uploaded_data as the field name. + Example: + <%= form.file_field :uploaded_data %> + +Displaying uploaded images is made easy by the public_filename method of the ActiveRecord attachment objects using file system and s3 storage. + +public_filename(thumbnail = nil) + Returns the public path to the file. If a thumbnail prefix is specified it will return the public file path to the corresponding thumbnail. + Examples: + attachment_obj.public_filename #=> /attachments/2/file.jpg + attachment_obj.public_filename(:thumb) #=> /attachments/2/file_thumb.jpg + attachment_obj.public_filename(:small) #=> /attachments/2/file_small.jpg + +When serving files from database storage, doing more than simply downloading the file is beyond the scope of this document. + + +attachment_fu controllers +========================= + +There are two considerations to take into account when using attachment_fu in controllers. + +The first is when the files have no publicly accessible path and need to be downloaded through an action. + +Example: + def readme + send_file '/path/to/readme.txt', :type => 'plain/text', :disposition => 'inline' + end + +See the possible values for send_file for reference. + + +The second is when saving the file when submitted from a form. +Example in view: + <%= form.file_field :attachable, :uploaded_data %> + +Example in controller: + def create + @attachable_file = AttachmentMetadataModel.new(params[:attachable]) + if @attachable_file.save + flash[:notice] = 'Attachment was successfully created.' + redirect_to attachable_url(@attachable_file) + else + render :action => :new + end + end diff --git a/vendor/plugins/attachment_fu/Rakefile b/vendor/plugins/attachment_fu/Rakefile new file mode 100644 index 0000000..0851dd4 --- /dev/null +++ b/vendor/plugins/attachment_fu/Rakefile @@ -0,0 +1,22 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the attachment_fu plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the attachment_fu plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'ActsAsAttachment' + rdoc.options << '--line-numbers --inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/vendor/plugins/attachment_fu/amazon_s3.yml.tpl b/vendor/plugins/attachment_fu/amazon_s3.yml.tpl new file mode 100644 index 0000000..81cb807 --- /dev/null +++ b/vendor/plugins/attachment_fu/amazon_s3.yml.tpl @@ -0,0 +1,14 @@ +development: + bucket_name: appname_development + access_key_id: + secret_access_key: + +test: + bucket_name: appname_test + access_key_id: + secret_access_key: + +production: + bucket_name: appname + access_key_id: + secret_access_key: diff --git a/vendor/plugins/attachment_fu/init.rb b/vendor/plugins/attachment_fu/init.rb new file mode 100644 index 0000000..0239e56 --- /dev/null +++ b/vendor/plugins/attachment_fu/init.rb @@ -0,0 +1,14 @@ +require 'tempfile' + +Tempfile.class_eval do + # overwrite so tempfiles use the extension of the basename. important for rmagick and image science + def make_tmpname(basename, n) + ext = nil + sprintf("%s%d-%d%s", basename.to_s.gsub(/\.\w+$/) { |s| ext = s; '' }, $$, n, ext) + end +end + +require 'geometry' +ActiveRecord::Base.send(:extend, Technoweenie::AttachmentFu::ActMethods) +Technoweenie::AttachmentFu.tempfile_path = ATTACHMENT_FU_TEMPFILE_PATH if Object.const_defined?(:ATTACHMENT_FU_TEMPFILE_PATH) +FileUtils.mkdir_p Technoweenie::AttachmentFu.tempfile_path \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/install.rb b/vendor/plugins/attachment_fu/install.rb new file mode 100644 index 0000000..2938164 --- /dev/null +++ b/vendor/plugins/attachment_fu/install.rb @@ -0,0 +1,5 @@ +require 'fileutils' + +s3_config = File.dirname(__FILE__) + '/../../../config/amazon_s3.yml' +FileUtils.cp File.dirname(__FILE__) + '/amazon_s3.yml.tpl', s3_config unless File.exist?(s3_config) +puts IO.read(File.join(File.dirname(__FILE__), 'README')) \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/lib/geometry.rb b/vendor/plugins/attachment_fu/lib/geometry.rb new file mode 100644 index 0000000..2d6e381 --- /dev/null +++ b/vendor/plugins/attachment_fu/lib/geometry.rb @@ -0,0 +1,93 @@ +# This Geometry class was yanked from RMagick. However, it lets ImageMagick handle the actual change_geometry. +# Use #new_dimensions_for to get new dimensons +# Used so I can use spiffy RMagick geometry strings with ImageScience +class Geometry + # ! and @ are removed until support for them is added + FLAGS = ['', '%', '<', '>']#, '!', '@'] + RFLAGS = { '%' => :percent, + '!' => :aspect, + '<' => :>, + '>' => :<, + '@' => :area } + + attr_accessor :width, :height, :x, :y, :flag + + def initialize(width=nil, height=nil, x=nil, y=nil, flag=nil) + # Support floating-point width and height arguments so Geometry + # objects can be used to specify Image#density= arguments. + raise ArgumentError, "width must be >= 0: #{width}" if width < 0 + raise ArgumentError, "height must be >= 0: #{height}" if height < 0 + @width = width.to_f + @height = height.to_f + @x = x.to_i + @y = y.to_i + @flag = flag + end + + # Construct an object from a geometry string + RE = /\A(\d*)(?:x(\d+))?([-+]\d+)?([-+]\d+)?([%!<>@]?)\Z/ + + def self.from_s(str) + raise(ArgumentError, "no geometry string specified") unless str + + if m = RE.match(str) + new(m[1].to_i, m[2].to_i, m[3].to_i, m[4].to_i, RFLAGS[m[5]]) + else + raise ArgumentError, "invalid geometry format" + end + end + + # Convert object to a geometry string + def to_s + str = '' + str << "%g" % @width if @width > 0 + str << 'x' if (@width > 0 || @height > 0) + str << "%g" % @height if @height > 0 + str << "%+d%+d" % [@x, @y] if (@x != 0 || @y != 0) + str << FLAGS[@flag.to_i] + end + + # attempts to get new dimensions for the current geometry string given these old dimensions. + # This doesn't implement the aspect flag (!) or the area flag (@). PDI + def new_dimensions_for(orig_width, orig_height) + new_width = orig_width + new_height = orig_height + + case @flag + when :percent + scale_x = @width.zero? ? 100 : @width + scale_y = @height.zero? ? @width : @height + new_width = scale_x.to_f * (orig_width.to_f / 100.0) + new_height = scale_y.to_f * (orig_height.to_f / 100.0) + when :<, :>, nil + scale_factor = + if new_width.zero? || new_height.zero? + 1.0 + else + if @width.nonzero? && @height.nonzero? + [@width.to_f / new_width.to_f, @height.to_f / new_height.to_f].min + else + @width.nonzero? ? (@width.to_f / new_width.to_f) : (@height.to_f / new_height.to_f) + end + end + new_width = scale_factor * new_width.to_f + new_height = scale_factor * new_height.to_f + new_width = orig_width if @flag && orig_width.send(@flag, new_width) + new_height = orig_height if @flag && orig_height.send(@flag, new_height) + end + + [new_width, new_height].collect! { |v| v.round } + end +end + +class Array + # allows you to get new dimensions for the current array of dimensions with a given geometry string + # + # [50, 64] / '40>' # => [40, 51] + def /(geometry) + raise ArgumentError, "Only works with a [width, height] pair" if size != 2 + raise ArgumentError, "Must pass a valid geometry string or object" unless geometry.is_a?(String) || geometry.is_a?(Geometry) + geometry = Geometry.from_s(geometry) if geometry.is_a?(String) + geometry.new_dimensions_for first, last + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu.rb b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu.rb new file mode 100644 index 0000000..c10369f --- /dev/null +++ b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu.rb @@ -0,0 +1,405 @@ +module Technoweenie # :nodoc: + module AttachmentFu # :nodoc: + @@default_processors = %w(ImageScience Rmagick MiniMagick) + @@tempfile_path = File.join(RAILS_ROOT, 'tmp', 'attachment_fu') + @@content_types = ['image/jpeg', 'image/pjpeg', 'image/gif', 'image/png', 'image/x-png', 'image/jpg'] + mattr_reader :content_types, :tempfile_path, :default_processors + mattr_writer :tempfile_path + + class ThumbnailError < StandardError; end + class AttachmentError < StandardError; end + + module ActMethods + # Options: + # * :content_type - Allowed content types. Allows all by default. Use :image to allow all standard image types. + # * :min_size - Minimum size allowed. 1 byte is the default. + # * :max_size - Maximum size allowed. 1.megabyte is the default. + # * :size - Range of sizes allowed. (1..1.megabyte) is the default. This overrides the :min_size and :max_size options. + # * :resize_to - Used by RMagick to resize images. Pass either an array of width/height, or a geometry string. + # * :thumbnails - Specifies a set of thumbnails to generate. This accepts a hash of filename suffixes and RMagick resizing options. + # * :thumbnail_class - Set what class to use for thumbnails. This attachment class is used by default. + # * :path_prefix - path to store the uploaded files. Uses public/#{table_name} by default for the filesystem, and just #{table_name} + # for the S3 backend. Setting this sets the :storage to :file_system. + # * :storage - Use :file_system to specify the attachment data is stored with the file system. Defaults to :db_system. + # + # Examples: + # has_attachment :max_size => 1.kilobyte + # has_attachment :size => 1.megabyte..2.megabytes + # has_attachment :content_type => 'application/pdf' + # has_attachment :content_type => ['application/pdf', 'application/msword', 'text/plain'] + # has_attachment :content_type => :image, :resize_to => [50,50] + # has_attachment :content_type => ['application/pdf', :image], :resize_to => 'x50' + # has_attachment :thumbnails => { :thumb => [50, 50], :geometry => 'x50' } + # has_attachment :storage => :file_system, :path_prefix => 'public/files' + # has_attachment :storage => :file_system, :path_prefix => 'public/files', + # :content_type => :image, :resize_to => [50,50] + # has_attachment :storage => :file_system, :path_prefix => 'public/files', + # :thumbnails => { :thumb => [50, 50], :geometry => 'x50' } + # has_attachment :storage => :s3 + def has_attachment(options = {}) + # this allows you to redefine the acts' options for each subclass, however + options[:min_size] ||= 1 + options[:max_size] ||= 1.megabyte + options[:size] ||= (options[:min_size]..options[:max_size]) + options[:thumbnails] ||= {} + options[:thumbnail_class] ||= self + options[:s3_access] ||= :public_read + options[:content_type] = [options[:content_type]].flatten.collect! { |t| t == :image ? Technoweenie::AttachmentFu.content_types : t }.flatten unless options[:content_type].nil? + + unless options[:thumbnails].is_a?(Hash) + raise ArgumentError, ":thumbnails option should be a hash: e.g. :thumbnails => { :foo => '50x50' }" + end + + # doing these shenanigans so that #attachment_options is available to processors and backends + class_inheritable_accessor :attachment_options + self.attachment_options = options + + # only need to define these once on a class + unless included_modules.include?(InstanceMethods) + attr_accessor :thumbnail_resize_options + + attachment_options[:storage] ||= (attachment_options[:file_system_path] || attachment_options[:path_prefix]) ? :file_system : :db_file + attachment_options[:path_prefix] ||= attachment_options[:file_system_path] + if attachment_options[:path_prefix].nil? + attachment_options[:path_prefix] = attachment_options[:storage] == :s3 ? table_name : File.join("public", table_name) + end + attachment_options[:path_prefix] = attachment_options[:path_prefix][1..-1] if options[:path_prefix].first == '/' + + with_options :foreign_key => 'parent_id' do |m| + m.has_many :thumbnails, :class_name => attachment_options[:thumbnail_class].to_s + m.belongs_to :parent, :class_name => base_class.to_s + end + before_destroy :destroy_thumbnails + + before_validation :set_size_from_temp_path + after_save :after_process_attachment + after_destroy :destroy_file + extend ClassMethods + include InstanceMethods + include Technoweenie::AttachmentFu::Backends.const_get("#{options[:storage].to_s.classify}Backend") + case attachment_options[:processor] + when :none + when nil + processors = Technoweenie::AttachmentFu.default_processors.dup + begin + include Technoweenie::AttachmentFu::Processors.const_get("#{processors.first}Processor") if processors.any? + rescue LoadError, MissingSourceFile + processors.shift + retry + end + else + begin + include Technoweenie::AttachmentFu::Processors.const_get("#{options[:processor].to_s.classify}Processor") + rescue LoadError, MissingSourceFile + puts "Problems loading #{options[:processor]}Processor: #{$!}" + end + end + after_validation :process_attachment + end + end + end + + module ClassMethods + delegate :content_types, :to => Technoweenie::AttachmentFu + + # Performs common validations for attachment models. + def validates_as_attachment + validates_presence_of :size, :content_type, :filename + validate :attachment_attributes_valid? + end + + # Returns true or false if the given content type is recognized as an image. + def image?(content_type) + content_types.include?(content_type) + end + + # Callback after an image has been resized. + # + # class Foo < ActiveRecord::Base + # acts_as_attachment + # after_resize do |record, img| + # record.aspect_ratio = img.columns.to_f / img.rows.to_f + # end + # end + def after_resize(&block) + write_inheritable_array(:after_resize, [block]) + end + + # Callback after an attachment has been saved either to the file system or the DB. + # Only called if the file has been changed, not necessarily if the record is updated. + # + # class Foo < ActiveRecord::Base + # acts_as_attachment + # after_attachment_saved do |record| + # ... + # end + # end + def after_attachment_saved(&block) + write_inheritable_array(:after_attachment_saved, [block]) + end + + # Callback before a thumbnail is saved. Use this to pass any necessary extra attributes that may be required. + # + # class Foo < ActiveRecord::Base + # acts_as_attachment + # before_thumbnail_saved do |record, thumbnail| + # ... + # end + # end + def before_thumbnail_saved(&block) + write_inheritable_array(:before_thumbnail_saved, [block]) + end + + # Get the thumbnail class, which is the current attachment class by default. + # Configure this with the :thumbnail_class option. + def thumbnail_class + attachment_options[:thumbnail_class] = attachment_options[:thumbnail_class].constantize unless attachment_options[:thumbnail_class].is_a?(Class) + attachment_options[:thumbnail_class] + end + + # Copies the given file path to a new tempfile, returning the closed tempfile. + def copy_to_temp_file(file, temp_base_name) + returning Tempfile.new(temp_base_name, Technoweenie::AttachmentFu.tempfile_path) do |tmp| + tmp.close + FileUtils.cp file, tmp.path + end + end + + # Writes the given data to a new tempfile, returning the closed tempfile. + def write_to_temp_file(data, temp_base_name) + returning Tempfile.new(temp_base_name, Technoweenie::AttachmentFu.tempfile_path) do |tmp| + tmp.binmode + tmp.write data + tmp.close + end + end + end + + module InstanceMethods + # Checks whether the attachment's content type is an image content type + def image? + self.class.image?(content_type) + end + + # Returns true/false if an attachment is thumbnailable. A thumbnailable attachment has an image content type and the parent_id attribute. + def thumbnailable? + image? && respond_to?(:parent_id) && parent_id.nil? + end + + # Returns the class used to create new thumbnails for this attachment. + def thumbnail_class + self.class.thumbnail_class + end + + # Gets the thumbnail name for a filename. 'foo.jpg' becomes 'foo_thumbnail.jpg' + def thumbnail_name_for(thumbnail = nil) + return filename if thumbnail.blank? + ext = nil + basename = filename.gsub /\.\w+$/ do |s| + ext = s; '' + end + "#{basename}_#{thumbnail}#{ext}" + end + + # Creates or updates the thumbnail for the current attachment. + def create_or_update_thumbnail(temp_file, file_name_suffix, *size) + thumbnailable? || raise(ThumbnailError.new("Can't create a thumbnail if the content type is not an image or there is no parent_id column")) + returning find_or_initialize_thumbnail(file_name_suffix) do |thumb| + thumb.attributes = { + :content_type => content_type, + :filename => thumbnail_name_for(file_name_suffix), + :temp_path => temp_file, + :thumbnail_resize_options => size + } + callback_with_args :before_thumbnail_saved, thumb + thumb.save! + end + end + + # Sets the content type. + def content_type=(new_type) + write_attribute :content_type, new_type.to_s.strip + end + + # Sanitizes a filename. + def filename=(new_name) + write_attribute :filename, sanitize_filename(new_name) + end + + # Returns the width/height in a suitable format for the image_tag helper: (100x100) + def image_size + [width.to_s, height.to_s] * 'x' + end + + # Returns true if the attachment data will be written to the storage system on the next save + def save_attachment? + File.file?(temp_path.to_s) + end + + # nil placeholder in case this field is used in a form. + def uploaded_data() nil; end + + # This method handles the uploaded file object. If you set the field name to uploaded_data, you don't need + # any special code in your controller. + # + # <% form_for :attachment, :html => { :multipart => true } do |f| -%> + #

<%= f.file_field :uploaded_data %>

+ #

<%= submit_tag :Save %> + # <% end -%> + # + # @attachment = Attachment.create! params[:attachment] + # + # TODO: Allow it to work with Merb tempfiles too. + def uploaded_data=(file_data) + return nil if file_data.nil? || file_data.size == 0 + self.content_type = file_data.content_type + self.filename = file_data.original_filename if respond_to?(:filename) + if file_data.is_a?(StringIO) + file_data.rewind + self.temp_data = file_data.read + else + self.temp_path = file_data.path + end + end + + # Gets the latest temp path from the collection of temp paths. While working with an attachment, + # multiple Tempfile objects may be created for various processing purposes (resizing, for example). + # An array of all the tempfile objects is stored so that the Tempfile instance is held on to until + # it's not needed anymore. The collection is cleared after saving the attachment. + def temp_path + p = temp_paths.first + p.respond_to?(:path) ? p.path : p.to_s + end + + # Gets an array of the currently used temp paths. Defaults to a copy of #full_filename. + def temp_paths + @temp_paths ||= (new_record? || !File.exist?(full_filename)) ? [] : [copy_to_temp_file(full_filename)] + end + + # Adds a new temp_path to the array. This should take a string or a Tempfile. This class makes no + # attempt to remove the files, so Tempfiles should be used. Tempfiles remove themselves when they go out of scope. + # You can also use string paths for temporary files, such as those used for uploaded files in a web server. + def temp_path=(value) + temp_paths.unshift value + temp_path + end + + # Gets the data from the latest temp file. This will read the file into memory. + def temp_data + save_attachment? ? File.read(temp_path) : nil + end + + # Writes the given data to a Tempfile and adds it to the collection of temp files. + def temp_data=(data) + self.temp_path = write_to_temp_file data unless data.nil? + end + + # Copies the given file to a randomly named Tempfile. + def copy_to_temp_file(file) + self.class.copy_to_temp_file file, random_tempfile_filename + end + + # Writes the given file to a randomly named Tempfile. + def write_to_temp_file(data) + self.class.write_to_temp_file data, random_tempfile_filename + end + + # Stub for creating a temp file from the attachment data. This should be defined in the backend module. + def create_temp_file() end + + # Allows you to work with a processed representation (RMagick, ImageScience, etc) of the attachment in a block. + # + # @attachment.with_image do |img| + # self.data = img.thumbnail(100, 100).to_blob + # end + # + def with_image(&block) + self.class.with_image(temp_path, &block) + end + + protected + # Generates a unique filename for a Tempfile. + def random_tempfile_filename + "#{rand Time.now.to_i}#{filename || 'attachment'}" + end + + def sanitize_filename(filename) + returning filename.strip do |name| + # NOTE: File.basename doesn't work right with Windows paths on Unix + # get only the filename, not the whole path + name.gsub! /^.*(\\|\/)/, '' + + # Finally, replace all non alphanumeric, underscore or periods with underscore + name.gsub! /[^\w\.\-]/, '_' + end + end + + # before_validation callback. + def set_size_from_temp_path + self.size = File.size(temp_path) if save_attachment? + end + + # validates the size and content_type attributes according to the current model's options + def attachment_attributes_valid? + [:size, :content_type].each do |attr_name| + enum = attachment_options[attr_name] + errors.add attr_name, ActiveRecord::Errors.default_error_messages[:inclusion] unless enum.nil? || enum.include?(send(attr_name)) + end + end + + # Initializes a new thumbnail with the given suffix. + def find_or_initialize_thumbnail(file_name_suffix) + respond_to?(:parent_id) ? + thumbnail_class.find_or_initialize_by_thumbnail_and_parent_id(file_name_suffix.to_s, id) : + thumbnail_class.find_or_initialize_by_thumbnail(file_name_suffix.to_s) + end + + # Stub for a #process_attachment method in a processor + def process_attachment + @saved_attachment = save_attachment? + end + + # Cleans up after processing. Thumbnails are created, the attachment is stored to the backend, and the temp_paths are cleared. + def after_process_attachment + if @saved_attachment + if respond_to?(:process_attachment_with_processing) && thumbnailable? && !attachment_options[:thumbnails].blank? && parent_id.nil? + temp_file = temp_path || create_temp_file + attachment_options[:thumbnails].each { |suffix, size| create_or_update_thumbnail(temp_file, suffix, *size) } + end + save_to_storage + @temp_paths.clear + @saved_attachment = nil + callback :after_attachment_saved + end + end + + # Resizes the given processed img object with either the attachment resize options or the thumbnail resize options. + def resize_image_or_thumbnail!(img) + if (!respond_to?(:parent_id) || parent_id.nil?) && attachment_options[:resize_to] # parent image + resize_image(img, attachment_options[:resize_to]) + elsif thumbnail_resize_options # thumbnail + resize_image(img, thumbnail_resize_options) + end + end + + # Yanked from ActiveRecord::Callbacks, modified so I can pass args to the callbacks besides self. + # Only accept blocks, however + def callback_with_args(method, arg = self) + notify(method) + + result = nil + callbacks_for(method).each do |callback| + result = callback.call(self, arg) + return false if result == false + end + + return result + end + + # Removes the thumbnails for the attachment, if it has any + def destroy_thumbnails + self.thumbnails.each { |thumbnail| thumbnail.destroy } if thumbnailable? + end + end + end +end diff --git a/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/db_file_backend.rb b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/db_file_backend.rb new file mode 100644 index 0000000..23881e7 --- /dev/null +++ b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/db_file_backend.rb @@ -0,0 +1,39 @@ +module Technoweenie # :nodoc: + module AttachmentFu # :nodoc: + module Backends + # Methods for DB backed attachments + module DbFileBackend + def self.included(base) #:nodoc: + Object.const_set(:DbFile, Class.new(ActiveRecord::Base)) unless Object.const_defined?(:DbFile) + base.belongs_to :db_file, :class_name => '::DbFile', :foreign_key => 'db_file_id' + end + + # Creates a temp file with the current db data. + def create_temp_file + write_to_temp_file current_data + end + + # Gets the current data from the database + def current_data + db_file.data + end + + protected + # Destroys the file. Called in the after_destroy callback + def destroy_file + db_file.destroy if db_file + end + + # Saves the data to the DbFile model + def save_to_storage + if save_attachment? + (db_file || build_db_file).data = temp_data + db_file.save! + self.class.update_all ['db_file_id = ?', self.db_file_id = db_file.id], ['id = ?', id] + end + true + end + end + end + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/file_system_backend.rb b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/file_system_backend.rb new file mode 100644 index 0000000..464b9c7 --- /dev/null +++ b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/file_system_backend.rb @@ -0,0 +1,97 @@ +require 'ftools' +module Technoweenie # :nodoc: + module AttachmentFu # :nodoc: + module Backends + # Methods for file system backed attachments + module FileSystemBackend + def self.included(base) #:nodoc: + base.before_update :rename_file + end + + # Gets the full path to the filename in this format: + # + # # This assumes a model name like MyModel + # # public/#{table_name} is the default filesystem path + # RAILS_ROOT/public/my_models/5/blah.jpg + # + # Overwrite this method in your model to customize the filename. + # The optional thumbnail argument will output the thumbnail's filename. + def full_filename(thumbnail = nil) + file_system_path = (thumbnail ? thumbnail_class : self).attachment_options[:path_prefix].to_s + File.join(RAILS_ROOT, file_system_path, *partitioned_path(thumbnail_name_for(thumbnail))) + end + + # Used as the base path that #public_filename strips off full_filename to create the public path + def base_path + @base_path ||= File.join(RAILS_ROOT, 'public') + end + + # The attachment ID used in the full path of a file + def attachment_path_id + ((respond_to?(:parent_id) && parent_id) || id).to_i + end + + # overrwrite this to do your own app-specific partitioning. + # you can thank Jamis Buck for this: http://www.37signals.com/svn/archives2/id_partitioning.php + def partitioned_path(*args) + ("%08d" % attachment_path_id).scan(/..../) + args + end + + # Gets the public path to the file + # The optional thumbnail argument will output the thumbnail's filename. + def public_filename(thumbnail = nil) + full_filename(thumbnail).gsub %r(^#{Regexp.escape(base_path)}), '' + end + + def filename=(value) + @old_filename = full_filename unless filename.nil? || @old_filename + write_attribute :filename, sanitize_filename(value) + end + + # Creates a temp file from the currently saved file. + def create_temp_file + copy_to_temp_file full_filename + end + + protected + # Destroys the file. Called in the after_destroy callback + def destroy_file + FileUtils.rm full_filename + # remove directory also if it is now empty + Dir.rmdir(File.dirname(full_filename)) if (Dir.entries(File.dirname(full_filename))-['.','..']).empty? + rescue + logger.info "Exception destroying #{full_filename.inspect}: [#{$!.class.name}] #{$1.to_s}" + logger.warn $!.backtrace.collect { |b| " > #{b}" }.join("\n") + end + + # Renames the given file before saving + def rename_file + return unless @old_filename && @old_filename != full_filename + if save_attachment? && File.exists?(@old_filename) + FileUtils.rm @old_filename + elsif File.exists?(@old_filename) + FileUtils.mv @old_filename, full_filename + end + @old_filename = nil + true + end + + # Saves the file to the file system + def save_to_storage + if save_attachment? + # TODO: This overwrites the file if it exists, maybe have an allow_overwrite option? + FileUtils.mkdir_p(File.dirname(full_filename)) + File.cp(temp_path, full_filename) + File.chmod(attachment_options[:chmod] || 0644, full_filename) + end + @old_filename = nil + true + end + + def current_data + File.file?(full_filename) ? File.read(full_filename) : nil + end + end + end + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/s3_backend.rb b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/s3_backend.rb new file mode 100644 index 0000000..b3c575e --- /dev/null +++ b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/s3_backend.rb @@ -0,0 +1,309 @@ +module Technoweenie # :nodoc: + module AttachmentFu # :nodoc: + module Backends + # = AWS::S3 Storage Backend + # + # Enables use of {Amazon's Simple Storage Service}[http://aws.amazon.com/s3] as a storage mechanism + # + # == Requirements + # + # Requires the {AWS::S3 Library}[http://amazon.rubyforge.org] for S3 by Marcel Molina Jr. installed either + # as a gem or a as a Rails plugin. + # + # == Configuration + # + # Configuration is done via RAILS_ROOT/config/amazon_s3.yml and is loaded according to the RAILS_ENV. + # The minimum connection options that you must specify are a bucket name, your access key id and your secret access key. + # If you don't already have your access keys, all you need to sign up for the S3 service is an account at Amazon. + # You can sign up for S3 and get access keys by visiting http://aws.amazon.com/s3. + # + # Example configuration (RAILS_ROOT/config/amazon_s3.yml) + # + # development: + # bucket_name: appname_development + # access_key_id: + # secret_access_key: + # + # test: + # bucket_name: appname_test + # access_key_id: + # secret_access_key: + # + # production: + # bucket_name: appname + # access_key_id: + # secret_access_key: + # + # You can change the location of the config path by passing a full path to the :s3_config_path option. + # + # has_attachment :storage => :s3, :s3_config_path => (RAILS_ROOT + '/config/s3.yml') + # + # === Required configuration parameters + # + # * :access_key_id - The access key id for your S3 account. Provided by Amazon. + # * :secret_access_key - The secret access key for your S3 account. Provided by Amazon. + # * :bucket_name - A unique bucket name (think of the bucket_name as being like a database name). + # + # If any of these required arguments is missing, a MissingAccessKey exception will be raised from AWS::S3. + # + # == About bucket names + # + # Bucket names have to be globaly unique across the S3 system. And you can only have up to 100 of them, + # so it's a good idea to think of a bucket as being like a database, hence the correspondance in this + # implementation to the development, test, and production environments. + # + # The number of objects you can store in a bucket is, for all intents and purposes, unlimited. + # + # === Optional configuration parameters + # + # * :server - The server to make requests to. Defaults to s3.amazonaws.com. + # * :port - The port to the requests should be made on. Defaults to 80 or 443 if :use_ssl is set. + # * :use_ssl - If set to true, :port will be implicitly set to 443, unless specified otherwise. Defaults to false. + # + # == Usage + # + # To specify S3 as the storage mechanism for a model, set the acts_as_attachment :storage option to :s3. + # + # class Photo < ActiveRecord::Base + # has_attachment :storage => :s3 + # end + # + # === Customizing the path + # + # By default, files are prefixed using a pseudo hierarchy in the form of :table_name/:id, which results + # in S3 urls that look like: http(s)://:server/:bucket_name/:table_name/:id/:filename with :table_name + # representing the customizable portion of the path. You can customize this prefix using the :path_prefix + # option: + # + # class Photo < ActiveRecord::Base + # has_attachment :storage => :s3, :path_prefix => 'my/custom/path' + # end + # + # Which would result in URLs like http(s)://:server/:bucket_name/my/custom/path/:id/:filename. + # + # === Permissions + # + # By default, files are stored on S3 with public access permissions. You can customize this using + # the :s3_access option to has_attachment. Available values are + # :private, :public_read_write, and :authenticated_read. + # + # === Other options + # + # Of course, all the usual configuration options apply, such as content_type and thumbnails: + # + # class Photo < ActiveRecord::Base + # has_attachment :storage => :s3, :content_type => ['application/pdf', :image], :resize_to => 'x50' + # has_attachment :storage => :s3, :thumbnails => { :thumb => [50, 50], :geometry => 'x50' } + # end + # + # === Accessing S3 URLs + # + # You can get an object's URL using the s3_url accessor. For example, assuming that for your postcard app + # you had a bucket name like 'postcard_world_development', and an attachment model called Photo: + # + # @postcard.s3_url # => http(s)://s3.amazonaws.com/postcard_world_development/photos/1/mexico.jpg + # + # The resulting url is in the form: http(s)://:server/:bucket_name/:table_name/:id/:file. + # The optional thumbnail argument will output the thumbnail's filename (if any). + # + # Additionally, you can get an object's base path relative to the bucket root using + # base_path: + # + # @photo.file_base_path # => photos/1 + # + # And the full path (including the filename) using full_filename: + # + # @photo.full_filename # => photos/ + # + # Niether base_path or full_filename include the bucket name as part of the path. + # You can retrieve the bucket name using the bucket_name method. + module S3Backend + class RequiredLibraryNotFoundError < StandardError; end + class ConfigFileNotFoundError < StandardError; end + + def self.included(base) #:nodoc: + mattr_reader :bucket_name, :s3_config + + begin + require 'aws/s3' + include AWS::S3 + rescue LoadError + raise RequiredLibraryNotFoundError.new('AWS::S3 could not be loaded') + end + + begin + @@s3_config_path = base.attachment_options[:s3_config_path] || (RAILS_ROOT + '/config/amazon_s3.yml') + @@s3_config = YAML.load_file(@@s3_config_path)[ENV['RAILS_ENV']].symbolize_keys + #rescue + # raise ConfigFileNotFoundError.new('File %s not found' % @@s3_config_path) + end + + @@bucket_name = s3_config[:bucket_name] + + Base.establish_connection!( + :access_key_id => s3_config[:access_key_id], + :secret_access_key => s3_config[:secret_access_key], + :server => s3_config[:server], + :port => s3_config[:port], + :use_ssl => s3_config[:use_ssl] + ) + + # Bucket.create(@@bucket_name) + + base.before_update :rename_file + end + + def self.protocol + @protocol ||= s3_config[:use_ssl] ? 'https://' : 'http://' + end + + def self.hostname + @hostname ||= s3_config[:server] || AWS::S3::DEFAULT_HOST + end + + def self.port_string + @port_string ||= s3_config[:port] == (s3_config[:use_ssl] ? 443 : 80) ? '' : ":#{s3_config[:port]}" + end + + module ClassMethods + def s3_protocol + Technoweenie::AttachmentFu::Backends::S3Backend.protocol + end + + def s3_hostname + Technoweenie::AttachmentFu::Backends::S3Backend.hostname + end + + def s3_port_string + Technoweenie::AttachmentFu::Backends::S3Backend.port_string + end + end + + # Overwrites the base filename writer in order to store the old filename + def filename=(value) + @old_filename = filename unless filename.nil? || @old_filename + write_attribute :filename, sanitize_filename(value) + end + + # The attachment ID used in the full path of a file + def attachment_path_id + ((respond_to?(:parent_id) && parent_id) || id).to_s + end + + # The pseudo hierarchy containing the file relative to the bucket name + # Example: :table_name/:id + def base_path + File.join(attachment_options[:path_prefix], attachment_path_id) + end + + # The full path to the file relative to the bucket name + # Example: :table_name/:id/:filename + def full_filename(thumbnail = nil) + File.join(base_path, thumbnail_name_for(thumbnail)) + end + + # All public objects are accessible via a GET request to the S3 servers. You can generate a + # url for an object using the s3_url method. + # + # @photo.s3_url + # + # The resulting url is in the form: http(s)://:server/:bucket_name/:table_name/:id/:file where + # the :server variable defaults to AWS::S3 URL::DEFAULT_HOST (s3.amazonaws.com) and can be + # set using the configuration parameters in RAILS_ROOT/config/amazon_s3.yml. + # + # The optional thumbnail argument will output the thumbnail's filename (if any). + def s3_url(thumbnail = nil) + File.join(s3_protocol + s3_hostname + s3_port_string, bucket_name, full_filename(thumbnail)) + end + alias :public_filename :s3_url + + # All private objects are accessible via an authenticated GET request to the S3 servers. You can generate an + # authenticated url for an object like this: + # + # @photo.authenticated_s3_url + # + # By default authenticated urls expire 5 minutes after they were generated. + # + # Expiration options can be specified either with an absolute time using the :expires option, + # or with a number of seconds relative to now with the :expires_in option: + # + # # Absolute expiration date (October 13th, 2025) + # @photo.authenticated_s3_url(:expires => Time.mktime(2025,10,13).to_i) + # + # # Expiration in five hours from now + # @photo.authenticated_s3_url(:expires_in => 5.hours) + # + # You can specify whether the url should go over SSL with the :use_ssl option. + # By default, the ssl settings for the current connection will be used: + # + # @photo.authenticated_s3_url(:use_ssl => true) + # + # Finally, the optional thumbnail argument will output the thumbnail's filename (if any): + # + # @photo.authenticated_s3_url('thumbnail', :expires_in => 5.hours, :use_ssl => true) + def authenticated_s3_url(*args) + thumbnail = args.first.is_a?(String) ? args.first : nil + options = args.last.is_a?(Hash) ? args.last : {} + S3Object.url_for(full_filename(thumbnail), bucket_name, options) + end + + def create_temp_file + write_to_temp_file current_data + end + + def current_data + S3Object.value full_filename, bucket_name + end + + def s3_protocol + Technoweenie::AttachmentFu::Backends::S3Backend.protocol + end + + def s3_hostname + Technoweenie::AttachmentFu::Backends::S3Backend.hostname + end + + def s3_port_string + Technoweenie::AttachmentFu::Backends::S3Backend.port_string + end + + protected + # Called in the after_destroy callback + def destroy_file + S3Object.delete full_filename, bucket_name + end + + def rename_file + return unless @old_filename && @old_filename != filename + + old_full_filename = File.join(base_path, @old_filename) + + S3Object.rename( + old_full_filename, + full_filename, + bucket_name, + :access => attachment_options[:s3_access] + ) + + @old_filename = nil + true + end + + def save_to_storage + if save_attachment? + S3Object.store( + full_filename, + (temp_path ? File.open(temp_path) : temp_data), + bucket_name, + :content_type => content_type, + :access => attachment_options[:s3_access] + ) + end + + @old_filename = nil + true + end + end + end + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/image_science_processor.rb b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/image_science_processor.rb new file mode 100644 index 0000000..37c1415 --- /dev/null +++ b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/image_science_processor.rb @@ -0,0 +1,55 @@ +require 'image_science' +module Technoweenie # :nodoc: + module AttachmentFu # :nodoc: + module Processors + module ImageScienceProcessor + def self.included(base) + base.send :extend, ClassMethods + base.alias_method_chain :process_attachment, :processing + end + + module ClassMethods + # Yields a block containing an RMagick Image for the given binary data. + def with_image(file, &block) + ::ImageScience.with_image file, &block + end + end + + protected + def process_attachment_with_processing + return unless process_attachment_without_processing && image? + with_image do |img| + self.width = img.width if respond_to?(:width) + self.height = img.height if respond_to?(:height) + resize_image_or_thumbnail! img + end + end + + # Performs the actual resizing operation for a thumbnail + def resize_image(img, size) + # create a dummy temp file to write to + filename.sub! /gif$/, 'png' + self.temp_path = write_to_temp_file(filename) + grab_dimensions = lambda do |img| + self.width = img.width if respond_to?(:width) + self.height = img.height if respond_to?(:height) + img.save temp_path + callback_with_args :after_resize, img + end + + size = size.first if size.is_a?(Array) && size.length == 1 + if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum)) + if size.is_a?(Fixnum) + img.thumbnail(size, &grab_dimensions) + else + img.resize(size[0], size[1], &grab_dimensions) + end + else + new_size = [img.width, img.height] / size.to_s + img.resize(new_size[0], new_size[1], &grab_dimensions) + end + end + end + end + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/mini_magick_processor.rb b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/mini_magick_processor.rb new file mode 100644 index 0000000..e5a534c --- /dev/null +++ b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/mini_magick_processor.rb @@ -0,0 +1,56 @@ +require 'mini_magick' +module Technoweenie # :nodoc: + module AttachmentFu # :nodoc: + module Processors + module MiniMagickProcessor + def self.included(base) + base.send :extend, ClassMethods + base.alias_method_chain :process_attachment, :processing + end + + module ClassMethods + # Yields a block containing an MiniMagick Image for the given binary data. + def with_image(file, &block) + begin + binary_data = file.is_a?(MiniMagick::Image) ? file : MiniMagick::Image.from_file(file) unless !Object.const_defined?(:MiniMagick) + rescue + # Log the failure to load the image. + logger.debug("Exception working with image: #{$!}") + binary_data = nil + end + block.call binary_data if block && binary_data + ensure + !binary_data.nil? + end + end + + protected + def process_attachment_with_processing + return unless process_attachment_without_processing + with_image do |img| + resize_image_or_thumbnail! img + self.width = img[:width] if respond_to?(:width) + self.height = img[:height] if respond_to?(:height) + callback_with_args :after_resize, img + end if image? + end + + # Performs the actual resizing operation for a thumbnail + def resize_image(img, size) + size = size.first if size.is_a?(Array) && size.length == 1 + if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum)) + if size.is_a?(Fixnum) + size = [size, size] + img.resize(size.join('x')) + else + img.resize(size.join('x') + '!') + end + else + img.resize(size.to_s) + end + self.temp_path = img + end + end + end + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/rmagick_processor.rb b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/rmagick_processor.rb new file mode 100644 index 0000000..7999edb --- /dev/null +++ b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/rmagick_processor.rb @@ -0,0 +1,53 @@ +require 'RMagick' +module Technoweenie # :nodoc: + module AttachmentFu # :nodoc: + module Processors + module RmagickProcessor + def self.included(base) + base.send :extend, ClassMethods + base.alias_method_chain :process_attachment, :processing + end + + module ClassMethods + # Yields a block containing an RMagick Image for the given binary data. + def with_image(file, &block) + begin + binary_data = file.is_a?(Magick::Image) ? file : Magick::Image.read(file).first unless !Object.const_defined?(:Magick) + rescue + # Log the failure to load the image. This should match ::Magick::ImageMagickError + # but that would cause acts_as_attachment to require rmagick. + logger.debug("Exception working with image: #{$!}") + binary_data = nil + end + block.call binary_data if block && binary_data + ensure + !binary_data.nil? + end + end + + protected + def process_attachment_with_processing + return unless process_attachment_without_processing + with_image do |img| + resize_image_or_thumbnail! img + self.width = img.columns if respond_to?(:width) + self.height = img.rows if respond_to?(:height) + callback_with_args :after_resize, img + end if image? + end + + # Performs the actual resizing operation for a thumbnail + def resize_image(img, size) + size = size.first if size.is_a?(Array) && size.length == 1 && !size.first.is_a?(Fixnum) + if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum)) + size = [size, size] if size.is_a?(Fixnum) + img.thumbnail!(*size) + else + img.change_geometry(size.to_s) { |cols, rows, image| image.resize!(cols, rows) } + end + self.temp_path = write_to_temp_file(img.to_blob) + end + end + end + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/amazon_s3.yml b/vendor/plugins/attachment_fu/test/amazon_s3.yml new file mode 100644 index 0000000..0024c8e --- /dev/null +++ b/vendor/plugins/attachment_fu/test/amazon_s3.yml @@ -0,0 +1,6 @@ +test: + bucket_name: afu + access_key_id: YOURACCESSKEY + secret_access_key: YOURSECRETACCESSKEY + server: 127.0.0.1 + port: 3002 \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/backends/db_file_test.rb b/vendor/plugins/attachment_fu/test/backends/db_file_test.rb new file mode 100644 index 0000000..e95bb49 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/backends/db_file_test.rb @@ -0,0 +1,16 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper')) + +class DbFileTest < Test::Unit::TestCase + include BaseAttachmentTests + attachment_model Attachment + + def test_should_call_after_attachment_saved(klass = Attachment) + attachment_model.saves = 0 + assert_created do + upload_file :filename => '/files/rails.png' + end + assert_equal 1, attachment_model.saves + end + + test_against_subclass :test_should_call_after_attachment_saved, Attachment +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/backends/file_system_test.rb b/vendor/plugins/attachment_fu/test/backends/file_system_test.rb new file mode 100644 index 0000000..d3250c1 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/backends/file_system_test.rb @@ -0,0 +1,80 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper')) + +class FileSystemTest < Test::Unit::TestCase + include BaseAttachmentTests + attachment_model FileAttachment + + def test_filesystem_size_for_file_attachment(klass = FileAttachment) + attachment_model klass + assert_created 1 do + attachment = upload_file :filename => '/files/rails.png' + assert_equal attachment.size, File.open(attachment.full_filename).stat.size + end + end + + test_against_subclass :test_filesystem_size_for_file_attachment, FileAttachment + + def test_should_not_overwrite_file_attachment(klass = FileAttachment) + attachment_model klass + assert_created 2 do + real = upload_file :filename => '/files/rails.png' + assert_valid real + assert !real.new_record?, real.errors.full_messages.join("\n") + assert !real.size.zero? + + fake = upload_file :filename => '/files/fake/rails.png' + assert_valid fake + assert !fake.size.zero? + + assert_not_equal File.open(real.full_filename).stat.size, File.open(fake.full_filename).stat.size + end + end + + test_against_subclass :test_should_not_overwrite_file_attachment, FileAttachment + + def test_should_store_file_attachment_in_filesystem(klass = FileAttachment) + attachment_model klass + attachment = nil + assert_created do + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + assert File.exists?(attachment.full_filename), "#{attachment.full_filename} does not exist" + end + attachment + end + + test_against_subclass :test_should_store_file_attachment_in_filesystem, FileAttachment + + def test_should_delete_old_file_when_updating(klass = FileAttachment) + attachment_model klass + attachment = upload_file :filename => '/files/rails.png' + old_filename = attachment.full_filename + assert_not_created do + use_temp_file 'files/rails.png' do |file| + attachment.filename = 'rails2.png' + attachment.temp_path = File.join(fixture_path, file) + attachment.save! + assert File.exists?(attachment.full_filename), "#{attachment.full_filename} does not exist" + assert !File.exists?(old_filename), "#{old_filename} still exists" + end + end + end + + test_against_subclass :test_should_delete_old_file_when_updating, FileAttachment + + def test_should_delete_old_file_when_renaming(klass = FileAttachment) + attachment_model klass + attachment = upload_file :filename => '/files/rails.png' + old_filename = attachment.full_filename + assert_not_created do + attachment.filename = 'rails2.png' + attachment.save + assert File.exists?(attachment.full_filename), "#{attachment.full_filename} does not exist" + assert !File.exists?(old_filename), "#{old_filename} still exists" + assert !attachment.reload.size.zero? + assert_equal 'rails2.png', attachment.filename + end + end + + test_against_subclass :test_should_delete_old_file_when_renaming, FileAttachment +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/backends/remote/s3_test.rb b/vendor/plugins/attachment_fu/test/backends/remote/s3_test.rb new file mode 100644 index 0000000..82520a0 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/backends/remote/s3_test.rb @@ -0,0 +1,103 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'test_helper')) +require 'net/http' + +class S3Test < Test::Unit::TestCase + if File.exist?(File.join(File.dirname(__FILE__), '../../amazon_s3.yml')) + include BaseAttachmentTests + attachment_model S3Attachment + + def test_should_create_correct_bucket_name(klass = S3Attachment) + attachment_model klass + attachment = upload_file :filename => '/files/rails.png' + assert_equal attachment.s3_config[:bucket_name], attachment.bucket_name + end + + test_against_subclass :test_should_create_correct_bucket_name, S3Attachment + + def test_should_create_default_path_prefix(klass = S3Attachment) + attachment_model klass + attachment = upload_file :filename => '/files/rails.png' + assert_equal File.join(attachment_model.table_name, attachment.attachment_path_id), attachment.base_path + end + + test_against_subclass :test_should_create_default_path_prefix, S3Attachment + + def test_should_create_custom_path_prefix(klass = S3WithPathPrefixAttachment) + attachment_model klass + attachment = upload_file :filename => '/files/rails.png' + assert_equal File.join('some/custom/path/prefix', attachment.attachment_path_id), attachment.base_path + end + + test_against_subclass :test_should_create_custom_path_prefix, S3WithPathPrefixAttachment + + def test_should_create_valid_url(klass = S3Attachment) + attachment_model klass + attachment = upload_file :filename => '/files/rails.png' + assert_equal "#{s3_protocol}#{s3_hostname}#{s3_port_string}/#{attachment.bucket_name}/#{attachment.full_filename}", attachment.s3_url + end + + test_against_subclass :test_should_create_valid_url, S3Attachment + + def test_should_create_authenticated_url(klass = S3Attachment) + attachment_model klass + attachment = upload_file :filename => '/files/rails.png' + assert_match /^http.+AWSAccessKeyId.+Expires.+Signature.+/, attachment.authenticated_s3_url(:use_ssl => true) + end + + test_against_subclass :test_should_create_authenticated_url, S3Attachment + + def test_should_save_attachment(klass = S3Attachment) + attachment_model klass + assert_created do + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + assert attachment.image? + assert !attachment.size.zero? + assert_kind_of Net::HTTPOK, http_response_for(attachment.s3_url) + end + end + + test_against_subclass :test_should_save_attachment, S3Attachment + + def test_should_delete_attachment_from_s3_when_attachment_record_destroyed(klass = S3Attachment) + attachment_model klass + attachment = upload_file :filename => '/files/rails.png' + + urls = [attachment.s3_url] + attachment.thumbnails.collect(&:s3_url) + + urls.each {|url| assert_kind_of Net::HTTPOK, http_response_for(url) } + attachment.destroy + urls.each do |url| + begin + http_response_for(url) + rescue Net::HTTPForbidden, Net::HTTPNotFound + nil + end + end + end + + test_against_subclass :test_should_delete_attachment_from_s3_when_attachment_record_destroyed, S3Attachment + + protected + def http_response_for(url) + url = URI.parse(url) + Net::HTTP.start(url.host, url.port) {|http| http.request_head(url.path) } + end + + def s3_protocol + Technoweenie::AttachmentFu::Backends::S3Backend.protocol + end + + def s3_hostname + Technoweenie::AttachmentFu::Backends::S3Backend.hostname + end + + def s3_port_string + Technoweenie::AttachmentFu::Backends::S3Backend.port_string + end + else + def test_flunk_s3 + puts "s3 config file not loaded, tests not running" + end + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/base_attachment_tests.rb b/vendor/plugins/attachment_fu/test/base_attachment_tests.rb new file mode 100644 index 0000000..c9dbbd7 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/base_attachment_tests.rb @@ -0,0 +1,57 @@ +module BaseAttachmentTests + def test_should_create_file_from_uploaded_file + assert_created do + attachment = upload_file :filename => '/files/foo.txt' + assert_valid attachment + assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file) + assert attachment.image? + assert !attachment.size.zero? + #assert_equal 3, attachment.size + assert_nil attachment.width + assert_nil attachment.height + end + end + + def test_reassign_attribute_data + assert_created 1 do + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + assert attachment.size > 0, "no data was set" + + attachment.temp_data = 'wtf' + assert attachment.save_attachment? + attachment.save! + + assert_equal 'wtf', attachment_model.find(attachment.id).send(:current_data) + end + end + + def test_no_reassign_attribute_data_on_nil + assert_created 1 do + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + assert attachment.size > 0, "no data was set" + + attachment.temp_data = nil + assert !attachment.save_attachment? + end + end + + def test_should_overwrite_old_contents_when_updating + attachment = upload_file :filename => '/files/rails.png' + assert_not_created do # no new db_file records + use_temp_file 'files/rails.png' do |file| + attachment.filename = 'rails2.png' + attachment.temp_path = File.join(fixture_path, file) + attachment.save! + end + end + end + + def test_should_save_without_updating_file + attachment = upload_file :filename => '/files/foo.txt' + assert_valid attachment + assert !attachment.save_attachment? + assert_nothing_raised { attachment.save! } + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/basic_test.rb b/vendor/plugins/attachment_fu/test/basic_test.rb new file mode 100644 index 0000000..2094eb1 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/basic_test.rb @@ -0,0 +1,64 @@ +require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper')) + +class BasicTest < Test::Unit::TestCase + def test_should_set_default_min_size + assert_equal 1, Attachment.attachment_options[:min_size] + end + + def test_should_set_default_max_size + assert_equal 1.megabyte, Attachment.attachment_options[:max_size] + end + + def test_should_set_default_size + assert_equal (1..1.megabyte), Attachment.attachment_options[:size] + end + + def test_should_set_default_thumbnails_option + assert_equal Hash.new, Attachment.attachment_options[:thumbnails] + end + + def test_should_set_default_thumbnail_class + assert_equal Attachment, Attachment.attachment_options[:thumbnail_class] + end + + def test_should_normalize_content_types_to_array + assert_equal %w(pdf), PdfAttachment.attachment_options[:content_type] + assert_equal %w(pdf doc txt), DocAttachment.attachment_options[:content_type] + assert_equal ['image/jpeg', 'image/pjpeg', 'image/gif', 'image/png', 'image/x-png'], ImageAttachment.attachment_options[:content_type] + assert_equal ['pdf', 'image/jpeg', 'image/pjpeg', 'image/gif', 'image/png', 'image/x-png'], ImageOrPdfAttachment.attachment_options[:content_type] + end + + def test_should_sanitize_content_type + @attachment = Attachment.new :content_type => ' foo ' + assert_equal 'foo', @attachment.content_type + end + + def test_should_sanitize_filenames + @attachment = Attachment.new :filename => 'blah/foo.bar' + assert_equal 'foo.bar', @attachment.filename + + @attachment.filename = 'blah\\foo.bar' + assert_equal 'foo.bar', @attachment.filename + + @attachment.filename = 'f o!O-.bar' + assert_equal 'f_o_O-.bar', @attachment.filename + end + + def test_should_convert_thumbnail_name + @attachment = FileAttachment.new :filename => 'foo.bar' + assert_equal 'foo.bar', @attachment.thumbnail_name_for(nil) + assert_equal 'foo.bar', @attachment.thumbnail_name_for('') + assert_equal 'foo_blah.bar', @attachment.thumbnail_name_for(:blah) + assert_equal 'foo_blah.blah.bar', @attachment.thumbnail_name_for('blah.blah') + + @attachment.filename = 'foo.bar.baz' + assert_equal 'foo.bar_blah.baz', @attachment.thumbnail_name_for(:blah) + end + + def test_should_require_valid_thumbnails_option + klass = Class.new(ActiveRecord::Base) + assert_raise ArgumentError do + klass.has_attachment :thumbnails => [] + end + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/database.yml b/vendor/plugins/attachment_fu/test/database.yml new file mode 100644 index 0000000..1c6ece7 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/database.yml @@ -0,0 +1,18 @@ +sqlite: + :adapter: sqlite + :dbfile: attachment_fu_plugin.sqlite.db +sqlite3: + :adapter: sqlite3 + :dbfile: attachment_fu_plugin.sqlite3.db +postgresql: + :adapter: postgresql + :username: postgres + :password: postgres + :database: attachment_fu_plugin_test + :min_messages: ERROR +mysql: + :adapter: mysql + :host: localhost + :username: rails + :password: + :database: attachment_fu_plugin_test \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/extra_attachment_test.rb b/vendor/plugins/attachment_fu/test/extra_attachment_test.rb new file mode 100644 index 0000000..15b1852 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/extra_attachment_test.rb @@ -0,0 +1,57 @@ +require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper')) + +class OrphanAttachmentTest < Test::Unit::TestCase + include BaseAttachmentTests + attachment_model OrphanAttachment + + def test_should_create_image_from_uploaded_file + assert_created do + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file) + assert attachment.image? + assert !attachment.size.zero? + end + end + + def test_should_create_file_from_uploaded_file + assert_created do + attachment = upload_file :filename => '/files/foo.txt' + assert_valid attachment + assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file) + assert attachment.image? + assert !attachment.size.zero? + end + end + + def test_should_create_image_from_uploaded_file_with_custom_content_type + assert_created do + attachment = upload_file :content_type => 'foo/bar', :filename => '/files/rails.png' + assert_valid attachment + assert !attachment.image? + assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file) + assert !attachment.size.zero? + #assert_equal 1784, attachment.size + end + end + + def test_should_create_thumbnail + attachment = upload_file :filename => '/files/rails.png' + + assert_raise Technoweenie::AttachmentFu::ThumbnailError do + attachment.create_or_update_thumbnail(attachment.create_temp_file, 'thumb', 50, 50) + end + end + + def test_should_create_thumbnail_with_geometry_string + attachment = upload_file :filename => '/files/rails.png' + + assert_raise Technoweenie::AttachmentFu::ThumbnailError do + attachment.create_or_update_thumbnail(attachment.create_temp_file, 'thumb', 'x50') + end + end +end + +class MinimalAttachmentTest < OrphanAttachmentTest + attachment_model MinimalAttachment +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/fixtures/attachment.rb b/vendor/plugins/attachment_fu/test/fixtures/attachment.rb new file mode 100644 index 0000000..77d60c3 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/fixtures/attachment.rb @@ -0,0 +1,127 @@ +class Attachment < ActiveRecord::Base + @@saves = 0 + cattr_accessor :saves + has_attachment :processor => :rmagick + validates_as_attachment + after_attachment_saved do |record| + self.saves += 1 + end +end + +class SmallAttachment < Attachment + has_attachment :max_size => 1.kilobyte +end + +class BigAttachment < Attachment + has_attachment :size => 1.megabyte..2.megabytes +end + +class PdfAttachment < Attachment + has_attachment :content_type => 'pdf' +end + +class DocAttachment < Attachment + has_attachment :content_type => %w(pdf doc txt) +end + +class ImageAttachment < Attachment + has_attachment :content_type => :image, :resize_to => [50,50] +end + +class ImageOrPdfAttachment < Attachment + has_attachment :content_type => ['pdf', :image], :resize_to => 'x50' +end + +class ImageWithThumbsAttachment < Attachment + has_attachment :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }, :resize_to => [55,55] + after_resize do |record, img| + record.aspect_ratio = img.columns.to_f / img.rows.to_f + end +end + +class FileAttachment < ActiveRecord::Base + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files', :processor => :rmagick + validates_as_attachment +end + +class ImageFileAttachment < FileAttachment + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files', + :content_type => :image, :resize_to => [50,50] +end + +class ImageWithThumbsFileAttachment < FileAttachment + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files', + :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }, :resize_to => [55,55] + after_resize do |record, img| + record.aspect_ratio = img.columns.to_f / img.rows.to_f + end +end + +class ImageWithThumbsClassFileAttachment < FileAttachment + # use file_system_path to test backwards compatibility + has_attachment :file_system_path => 'vendor/plugins/attachment_fu/test/files', + :thumbnails => { :thumb => [50, 50] }, :resize_to => [55,55], + :thumbnail_class => 'ImageThumbnail' +end + +class ImageThumbnail < FileAttachment + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files/thumbnails' +end + +# no parent +class OrphanAttachment < ActiveRecord::Base + has_attachment :processor => :rmagick + validates_as_attachment +end + +# no filename, no size, no content_type +class MinimalAttachment < ActiveRecord::Base + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files', :processor => :rmagick + validates_as_attachment + + def filename + "#{id}.file" + end +end + +begin + class ImageScienceAttachment < ActiveRecord::Base + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files', + :processor => :image_science, :thumbnails => { :thumb => [50, 51], :geometry => '31>' }, :resize_to => 55 + end +rescue MissingSourceFile + puts $!.message + puts "no ImageScience" +end + +begin + class MiniMagickAttachment < ActiveRecord::Base + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files', + :processor => :mini_magick, :thumbnails => { :thumb => [50, 51], :geometry => '31>' }, :resize_to => 55 + end +rescue MissingSourceFile + puts $!.message + puts "no Mini Magick" +end + +begin + class MiniMagickAttachment < ActiveRecord::Base + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files', + :processor => :mini_magick, :thumbnails => { :thumb => [50, 51], :geometry => '31>' }, :resize_to => 55 + end +rescue MissingSourceFile +end + +begin + class S3Attachment < ActiveRecord::Base + has_attachment :storage => :s3, :processor => :rmagick, :s3_config_path => File.join(File.dirname(__FILE__), '../amazon_s3.yml') + validates_as_attachment + end + + class S3WithPathPrefixAttachment < S3Attachment + has_attachment :storage => :s3, :path_prefix => 'some/custom/path/prefix', :processor => :rmagick + validates_as_attachment + end +rescue Technoweenie::AttachmentFu::Backends::S3Backend::ConfigFileNotFoundError + puts "S3 error: #{$!}" +end diff --git a/vendor/plugins/attachment_fu/test/fixtures/files/fake/rails.png b/vendor/plugins/attachment_fu/test/fixtures/files/fake/rails.png new file mode 100644 index 0000000..0543c64 Binary files /dev/null and b/vendor/plugins/attachment_fu/test/fixtures/files/fake/rails.png differ diff --git a/vendor/plugins/attachment_fu/test/fixtures/files/foo.txt b/vendor/plugins/attachment_fu/test/fixtures/files/foo.txt new file mode 100644 index 0000000..1910281 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/fixtures/files/foo.txt @@ -0,0 +1 @@ +foo \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/fixtures/files/rails.png b/vendor/plugins/attachment_fu/test/fixtures/files/rails.png new file mode 100644 index 0000000..b8441f1 Binary files /dev/null and b/vendor/plugins/attachment_fu/test/fixtures/files/rails.png differ diff --git a/vendor/plugins/attachment_fu/test/geometry_test.rb b/vendor/plugins/attachment_fu/test/geometry_test.rb new file mode 100644 index 0000000..ade4f48 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/geometry_test.rb @@ -0,0 +1,101 @@ +require 'test/unit' +require File.expand_path(File.join(File.dirname(__FILE__), '../lib/geometry')) unless Object.const_defined?(:Geometry) + +class GeometryTest < Test::Unit::TestCase + def test_should_resize + assert_geometry 50, 64, + "50x50" => [39, 50], + "60x60" => [47, 60], + "100x100" => [78, 100] + end + + def test_should_resize_no_width + assert_geometry 50, 64, + "x50" => [39, 50], + "x60" => [47, 60], + "x100" => [78, 100] + end + + def test_should_resize_no_height + assert_geometry 50, 64, + "50" => [50, 64], + "60" => [60, 77], + "100" => [100, 128] + end + + def test_should_resize_with_percent + assert_geometry 50, 64, + "50x50%" => [25, 32], + "60x60%" => [30, 38], + "120x112%" => [60, 72] + end + + def test_should_resize_with_percent_and_no_width + assert_geometry 50, 64, + "x50%" => [50, 32], + "x60%" => [50, 38], + "x112%" => [50, 72] + end + + def test_should_resize_with_percent_and_no_height + assert_geometry 50, 64, + "50%" => [25, 32], + "60%" => [30, 38], + "120%" => [60, 77] + end + + def test_should_resize_with_less + assert_geometry 50, 64, + "50x50<" => [50, 64], + "60x60<" => [50, 64], + "100x100<" => [78, 100], + "100x112<" => [88, 112], + "40x70<" => [50, 64] + end + + def test_should_resize_with_less_and_no_width + assert_geometry 50, 64, + "x50<" => [50, 64], + "x60<" => [50, 64], + "x100<" => [78, 100] + end + + def test_should_resize_with_less_and_no_height + assert_geometry 50, 64, + "50<" => [50, 64], + "60<" => [60, 77], + "100<" => [100, 128] + end + + def test_should_resize_with_greater + assert_geometry 50, 64, + "50x50>" => [39, 50], + "60x60>" => [47, 60], + "100x100>" => [50, 64], + "100x112>" => [50, 64], + "40x70>" => [40, 51] + end + + def test_should_resize_with_greater_and_no_width + assert_geometry 50, 64, + "x40>" => [31, 40], + "x60>" => [47, 60], + "x100>" => [50, 64] + end + + def test_should_resize_with_greater_and_no_height + assert_geometry 50, 64, + "40>" => [40, 51], + "60>" => [50, 64], + "100>" => [50, 64] + end + + protected + def assert_geometry(width, height, values) + values.each do |geo, result| + # run twice to verify the Geometry string isn't modified after a run + geo = Geometry.from_s(geo) + 2.times { assert_equal result, [width, height] / geo } + end + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/processors/image_science_test.rb b/vendor/plugins/attachment_fu/test/processors/image_science_test.rb new file mode 100644 index 0000000..636918d --- /dev/null +++ b/vendor/plugins/attachment_fu/test/processors/image_science_test.rb @@ -0,0 +1,31 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper')) + +class ImageScienceTest < Test::Unit::TestCase + attachment_model ImageScienceAttachment + + if Object.const_defined?(:ImageScience) + def test_should_resize_image + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + assert attachment.image? + # test image science thumbnail + assert_equal 42, attachment.width + assert_equal 55, attachment.height + + thumb = attachment.thumbnails.detect { |t| t.filename =~ /_thumb/ } + geo = attachment.thumbnails.detect { |t| t.filename =~ /_geometry/ } + + # test exact resize dimensions + assert_equal 50, thumb.width + assert_equal 51, thumb.height + + # test geometry string + assert_equal 31, geo.width + assert_equal 41, geo.height + end + else + def test_flunk + puts "ImageScience not loaded, tests not running" + end + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/processors/mini_magick_test.rb b/vendor/plugins/attachment_fu/test/processors/mini_magick_test.rb new file mode 100644 index 0000000..244a4a2 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/processors/mini_magick_test.rb @@ -0,0 +1,31 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper')) + +class MiniMagickTest < Test::Unit::TestCase + attachment_model MiniMagickAttachment + + if Object.const_defined?(:MiniMagick) + def test_should_resize_image + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + assert attachment.image? + # test MiniMagick thumbnail + assert_equal 43, attachment.width + assert_equal 55, attachment.height + + thumb = attachment.thumbnails.detect { |t| t.filename =~ /_thumb/ } + geo = attachment.thumbnails.detect { |t| t.filename =~ /_geometry/ } + + # test exact resize dimensions + assert_equal 50, thumb.width + assert_equal 51, thumb.height + + # test geometry string + assert_equal 31, geo.width + assert_equal 40, geo.height + end + else + def test_flunk + puts "MiniMagick not loaded, tests not running" + end + end +end diff --git a/vendor/plugins/attachment_fu/test/processors/rmagick_test.rb b/vendor/plugins/attachment_fu/test/processors/rmagick_test.rb new file mode 100644 index 0000000..13d177a --- /dev/null +++ b/vendor/plugins/attachment_fu/test/processors/rmagick_test.rb @@ -0,0 +1,241 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper')) + +class RmagickTest < Test::Unit::TestCase + attachment_model Attachment + + if Object.const_defined?(:Magick) + def test_should_create_image_from_uploaded_file + assert_created do + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file) + assert attachment.image? + assert !attachment.size.zero? + #assert_equal 1784, attachment.size + assert_equal 50, attachment.width + assert_equal 64, attachment.height + assert_equal '50x64', attachment.image_size + end + end + + def test_should_create_image_from_uploaded_file_with_custom_content_type + assert_created do + attachment = upload_file :content_type => 'foo/bar', :filename => '/files/rails.png' + assert_valid attachment + assert !attachment.image? + assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file) + assert !attachment.size.zero? + #assert_equal 1784, attachment.size + assert_nil attachment.width + assert_nil attachment.height + assert_equal [], attachment.thumbnails + end + end + + def test_should_create_thumbnail + attachment = upload_file :filename => '/files/rails.png' + + assert_created do + basename, ext = attachment.filename.split '.' + thumbnail = attachment.create_or_update_thumbnail(attachment.create_temp_file, 'thumb', 50, 50) + assert_valid thumbnail + assert !thumbnail.size.zero? + #assert_in_delta 4673, thumbnail.size, 2 + assert_equal 50, thumbnail.width + assert_equal 50, thumbnail.height + assert_equal [thumbnail.id], attachment.thumbnails.collect(&:id) + assert_equal attachment.id, thumbnail.parent_id if thumbnail.respond_to?(:parent_id) + assert_equal "#{basename}_thumb.#{ext}", thumbnail.filename + end + end + + def test_should_create_thumbnail_with_geometry_string + attachment = upload_file :filename => '/files/rails.png' + + assert_created do + basename, ext = attachment.filename.split '.' + thumbnail = attachment.create_or_update_thumbnail(attachment.create_temp_file, 'thumb', 'x50') + assert_valid thumbnail + assert !thumbnail.size.zero? + #assert_equal 3915, thumbnail.size + assert_equal 39, thumbnail.width + assert_equal 50, thumbnail.height + assert_equal [thumbnail], attachment.thumbnails + assert_equal attachment.id, thumbnail.parent_id if thumbnail.respond_to?(:parent_id) + assert_equal "#{basename}_thumb.#{ext}", thumbnail.filename + end + end + + def test_should_resize_image(klass = ImageAttachment) + attachment_model klass + assert_equal [50, 50], attachment_model.attachment_options[:resize_to] + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file) + assert attachment.image? + assert !attachment.size.zero? + #assert_in_delta 4673, attachment.size, 2 + assert_equal 50, attachment.width + assert_equal 50, attachment.height + end + + test_against_subclass :test_should_resize_image, ImageAttachment + + def test_should_resize_image_with_geometry(klass = ImageOrPdfAttachment) + attachment_model klass + assert_equal 'x50', attachment_model.attachment_options[:resize_to] + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file) + assert attachment.image? + assert !attachment.size.zero? + #assert_equal 3915, attachment.size + assert_equal 39, attachment.width + assert_equal 50, attachment.height + end + + test_against_subclass :test_should_resize_image_with_geometry, ImageOrPdfAttachment + + def test_should_give_correct_thumbnail_filenames(klass = ImageWithThumbsFileAttachment) + attachment_model klass + assert_created 3 do + attachment = upload_file :filename => '/files/rails.png' + thumb = attachment.thumbnails.detect { |t| t.filename =~ /_thumb/ } + geo = attachment.thumbnails.detect { |t| t.filename =~ /_geometry/ } + + [attachment, thumb, geo].each { |record| assert_valid record } + + assert_match /rails\.png$/, attachment.full_filename + assert_match /rails_geometry\.png$/, attachment.full_filename(:geometry) + assert_match /rails_thumb\.png$/, attachment.full_filename(:thumb) + end + end + + test_against_subclass :test_should_give_correct_thumbnail_filenames, ImageWithThumbsFileAttachment + + def test_should_automatically_create_thumbnails(klass = ImageWithThumbsAttachment) + attachment_model klass + assert_created 3 do + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + assert !attachment.size.zero? + #assert_equal 1784, attachment.size + assert_equal 55, attachment.width + assert_equal 55, attachment.height + assert_equal 2, attachment.thumbnails.length + assert_equal 1.0, attachment.aspect_ratio + + thumb = attachment.thumbnails.detect { |t| t.filename =~ /_thumb/ } + assert !thumb.new_record?, thumb.errors.full_messages.join("\n") + assert !thumb.size.zero? + #assert_in_delta 4673, thumb.size, 2 + assert_equal 50, thumb.width + assert_equal 50, thumb.height + assert_equal 1.0, thumb.aspect_ratio + + geo = attachment.thumbnails.detect { |t| t.filename =~ /_geometry/ } + assert !geo.new_record?, geo.errors.full_messages.join("\n") + assert !geo.size.zero? + #assert_equal 3915, geo.size + assert_equal 50, geo.width + assert_equal 50, geo.height + assert_equal 1.0, geo.aspect_ratio + end + end + + test_against_subclass :test_should_automatically_create_thumbnails, ImageWithThumbsAttachment + + # same as above method, but test it on a file model + test_against_class :test_should_automatically_create_thumbnails, ImageWithThumbsFileAttachment + test_against_subclass :test_should_automatically_create_thumbnails_on_class, ImageWithThumbsFileAttachment + + def test_should_use_thumbnail_subclass(klass = ImageWithThumbsClassFileAttachment) + attachment_model klass + attachment = nil + assert_difference ImageThumbnail, :count do + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + end + assert_kind_of ImageThumbnail, attachment.thumbnails.first + assert_equal attachment.id, attachment.thumbnails.first.parent.id + assert_kind_of FileAttachment, attachment.thumbnails.first.parent + assert_equal 'rails_thumb.png', attachment.thumbnails.first.filename + assert_equal attachment.thumbnails.first.full_filename, attachment.full_filename(attachment.thumbnails.first.thumbnail), + "#full_filename does not use thumbnail class' path." + assert_equal attachment.destroy attachment + end + + test_against_subclass :test_should_use_thumbnail_subclass, ImageWithThumbsClassFileAttachment + + def test_should_remove_old_thumbnail_files_when_updating(klass = ImageWithThumbsFileAttachment) + attachment_model klass + attachment = nil + assert_created 3 do + attachment = upload_file :filename => '/files/rails.png' + end + + old_filenames = [attachment.full_filename] + attachment.thumbnails.collect(&:full_filename) + + assert_not_created do + use_temp_file "files/rails.png" do |file| + attachment.filename = 'rails2.png' + attachment.temp_path = File.join(fixture_path, file) + attachment.save + new_filenames = [attachment.reload.full_filename] + attachment.thumbnails.collect { |t| t.reload.full_filename } + new_filenames.each { |f| assert File.exists?(f), "#{f} does not exist" } + old_filenames.each { |f| assert !File.exists?(f), "#{f} still exists" } + end + end + end + + test_against_subclass :test_should_remove_old_thumbnail_files_when_updating, ImageWithThumbsFileAttachment + + def test_should_delete_file_when_in_file_system_when_attachment_record_destroyed(klass = ImageWithThumbsFileAttachment) + attachment_model klass + attachment = upload_file :filename => '/files/rails.png' + filenames = [attachment.full_filename] + attachment.thumbnails.collect(&:full_filename) + filenames.each { |f| assert File.exists?(f), "#{f} never existed to delete on destroy" } + attachment.destroy + filenames.each { |f| assert !File.exists?(f), "#{f} still exists" } + end + + test_against_subclass :test_should_delete_file_when_in_file_system_when_attachment_record_destroyed, ImageWithThumbsFileAttachment + + def test_should_overwrite_old_thumbnail_records_when_updating(klass = ImageWithThumbsAttachment) + attachment_model klass + attachment = nil + assert_created 3 do + attachment = upload_file :filename => '/files/rails.png' + end + assert_not_created do # no new db_file records + use_temp_file "files/rails.png" do |file| + attachment.filename = 'rails2.png' + attachment.temp_path = File.join(fixture_path, file) + attachment.save! + end + end + end + + test_against_subclass :test_should_overwrite_old_thumbnail_records_when_updating, ImageWithThumbsAttachment + + def test_should_overwrite_old_thumbnail_records_when_renaming(klass = ImageWithThumbsAttachment) + attachment_model klass + attachment = nil + assert_created 3 do + attachment = upload_file :class => klass, :filename => '/files/rails.png' + end + assert_not_created do # no new db_file records + attachment.filename = 'rails2.png' + attachment.save + assert !attachment.reload.size.zero? + assert_equal 'rails2.png', attachment.filename + end + end + + test_against_subclass :test_should_overwrite_old_thumbnail_records_when_renaming, ImageWithThumbsAttachment + else + def test_flunk + puts "RMagick not installed, no tests running" + end + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/schema.rb b/vendor/plugins/attachment_fu/test/schema.rb new file mode 100644 index 0000000..b2e284d --- /dev/null +++ b/vendor/plugins/attachment_fu/test/schema.rb @@ -0,0 +1,86 @@ +ActiveRecord::Schema.define(:version => 0) do + create_table :attachments, :force => true do |t| + t.column :db_file_id, :integer + t.column :parent_id, :integer + t.column :thumbnail, :string + t.column :filename, :string, :limit => 255 + t.column :content_type, :string, :limit => 255 + t.column :size, :integer + t.column :width, :integer + t.column :height, :integer + t.column :aspect_ratio, :float + end + + create_table :file_attachments, :force => true do |t| + t.column :parent_id, :integer + t.column :thumbnail, :string + t.column :filename, :string, :limit => 255 + t.column :content_type, :string, :limit => 255 + t.column :size, :integer + t.column :width, :integer + t.column :height, :integer + t.column :type, :string + t.column :aspect_ratio, :float + end + + create_table :image_science_attachments, :force => true do |t| + t.column :parent_id, :integer + t.column :thumbnail, :string + t.column :filename, :string, :limit => 255 + t.column :content_type, :string, :limit => 255 + t.column :size, :integer + t.column :width, :integer + t.column :height, :integer + t.column :type, :string + end + + create_table :mini_magick_attachments, :force => true do |t| + t.column :parent_id, :integer + t.column :thumbnail, :string + t.column :filename, :string, :limit => 255 + t.column :content_type, :string, :limit => 255 + t.column :size, :integer + t.column :width, :integer + t.column :height, :integer + t.column :type, :string + end + + create_table :mini_magick_attachments, :force => true do |t| + t.column :parent_id, :integer + t.column :thumbnail, :string + t.column :filename, :string, :limit => 255 + t.column :content_type, :string, :limit => 255 + t.column :size, :integer + t.column :width, :integer + t.column :height, :integer + t.column :type, :string + end + + create_table :orphan_attachments, :force => true do |t| + t.column :db_file_id, :integer + t.column :filename, :string, :limit => 255 + t.column :content_type, :string, :limit => 255 + t.column :size, :integer + end + + create_table :minimal_attachments, :force => true do |t| + t.column :size, :integer + t.column :content_type, :string, :limit => 255 + end + + create_table :db_files, :force => true do |t| + t.column :data, :binary + end + + create_table :s3_attachments, :force => true do |t| + t.column :parent_id, :integer + t.column :thumbnail, :string + t.column :filename, :string, :limit => 255 + t.column :content_type, :string, :limit => 255 + t.column :size, :integer + t.column :width, :integer + t.column :height, :integer + t.column :type, :string + t.column :aspect_ratio, :float + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/test_helper.rb b/vendor/plugins/attachment_fu/test/test_helper.rb new file mode 100644 index 0000000..66a0b72 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/test_helper.rb @@ -0,0 +1,142 @@ +$:.unshift(File.dirname(__FILE__) + '/../lib') + +ENV['RAILS_ENV'] = 'test' + +require 'test/unit' +require File.expand_path(File.join(File.dirname(__FILE__), '../../../../config/environment.rb')) +require 'breakpoint' +require 'active_record/fixtures' +require 'action_controller/test_process' + +config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml')) +ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log") + +db_adapter = ENV['DB'] + +# no db passed, try one of these fine config-free DBs before bombing. +db_adapter ||= + begin + require 'rubygems' + require 'sqlite' + 'sqlite' + rescue MissingSourceFile + begin + require 'sqlite3' + 'sqlite3' + rescue MissingSourceFile + end + end + +if db_adapter.nil? + raise "No DB Adapter selected. Pass the DB= option to pick one, or install Sqlite or Sqlite3." +end + +ActiveRecord::Base.establish_connection(config[db_adapter]) + +load(File.dirname(__FILE__) + "/schema.rb") + +Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures" +$LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path) + +class Test::Unit::TestCase #:nodoc: + include ActionController::TestProcess + def create_fixtures(*table_names) + if block_given? + Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield } + else + Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) + end + end + + def setup + Attachment.saves = 0 + DbFile.transaction { [Attachment, FileAttachment, OrphanAttachment, MinimalAttachment, DbFile].each { |klass| klass.delete_all } } + attachment_model self.class.attachment_model + end + + def teardown + FileUtils.rm_rf File.join(File.dirname(__FILE__), 'files') + end + + self.use_transactional_fixtures = true + self.use_instantiated_fixtures = false + + def self.attachment_model(klass = nil) + @attachment_model = klass if klass + @attachment_model + end + + def self.test_against_class(test_method, klass, subclass = false) + define_method("#{test_method}_on_#{:sub if subclass}class") do + klass = Class.new(klass) if subclass + attachment_model klass + send test_method, klass + end + end + + def self.test_against_subclass(test_method, klass) + test_against_class test_method, klass, true + end + + protected + def upload_file(options = {}) + use_temp_file options[:filename] do |file| + att = attachment_model.create :uploaded_data => fixture_file_upload(file, options[:content_type] || 'image/png') + att.reload unless att.new_record? + return att + end + end + + def use_temp_file(fixture_filename) + temp_path = File.join('/tmp', File.basename(fixture_filename)) + FileUtils.mkdir_p File.join(fixture_path, 'tmp') + FileUtils.cp File.join(fixture_path, fixture_filename), File.join(fixture_path, temp_path) + yield temp_path + ensure + FileUtils.rm_rf File.join(fixture_path, 'tmp') + end + + def assert_created(num = 1) + assert_difference attachment_model.base_class, :count, num do + if attachment_model.included_modules.include? DbFile + assert_difference DbFile, :count, num do + yield + end + else + yield + end + end + end + + def assert_not_created + assert_created(0) { yield } + end + + def should_reject_by_size_with(klass) + attachment_model klass + assert_not_created do + attachment = upload_file :filename => '/files/rails.png' + assert attachment.new_record? + assert attachment.errors.on(:size) + assert_nil attachment.db_file if attachment.respond_to?(:db_file) + end + end + + def assert_difference(object, method = nil, difference = 1) + initial_value = object.send(method) + yield + assert_equal initial_value + difference, object.send(method) + end + + def assert_no_difference(object, method, &block) + assert_difference object, method, 0, &block + end + + def attachment_model(klass = nil) + @attachment_model = klass if klass + @attachment_model + end +end + +require File.join(File.dirname(__FILE__), 'fixtures/attachment') +require File.join(File.dirname(__FILE__), 'base_attachment_tests') \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/validation_test.rb b/vendor/plugins/attachment_fu/test/validation_test.rb new file mode 100644 index 0000000..a14cf99 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/validation_test.rb @@ -0,0 +1,55 @@ +require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper')) + +class ValidationTest < Test::Unit::TestCase + def test_should_invalidate_big_files + @attachment = SmallAttachment.new + assert !@attachment.valid? + assert @attachment.errors.on(:size) + + @attachment.size = 2000 + assert !@attachment.valid? + assert @attachment.errors.on(:size), @attachment.errors.full_messages.to_sentence + + @attachment.size = 1000 + assert !@attachment.valid? + assert_nil @attachment.errors.on(:size) + end + + def test_should_invalidate_small_files + @attachment = BigAttachment.new + assert !@attachment.valid? + assert @attachment.errors.on(:size) + + @attachment.size = 2000 + assert !@attachment.valid? + assert @attachment.errors.on(:size), @attachment.errors.full_messages.to_sentence + + @attachment.size = 1.megabyte + assert !@attachment.valid? + assert_nil @attachment.errors.on(:size) + end + + def test_should_validate_content_type + @attachment = PdfAttachment.new + assert !@attachment.valid? + assert @attachment.errors.on(:content_type) + + @attachment.content_type = 'foo' + assert !@attachment.valid? + assert @attachment.errors.on(:content_type) + + @attachment.content_type = 'pdf' + assert !@attachment.valid? + assert_nil @attachment.errors.on(:content_type) + end + + def test_should_require_filename + @attachment = Attachment.new + assert !@attachment.valid? + assert @attachment.errors.on(:filename) + + @attachment.filename = 'foo' + assert !@attachment.valid? + assert_nil @attachment.errors.on(:filename) + end +end \ No newline at end of file diff --git a/vendor/plugins/doc_browser/COPYING b/vendor/plugins/doc_browser/COPYING new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/vendor/plugins/doc_browser/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/vendor/plugins/doc_browser/README b/vendor/plugins/doc_browser/README new file mode 100644 index 0000000..0179303 --- /dev/null +++ b/vendor/plugins/doc_browser/README @@ -0,0 +1,56 @@ += doc_browser plugin for Rails + +Do you also think that's boring to browse your filesystem to reach the +documentation installed on your Rails application? Your probleams are over. + +Install doc_browser plugin and browse your documentation from Rails' +development server. + +== Usage + +Install the plugin: + + ./script/plugin install https://svn.colivre.coop.br/svn/rails/plugins/doc_browser + +The plugin installer will create a symbolic link in the public/ directory in +your Rails application, pointing to ../doc (your doc directory at the +application's root). If it doesn't (or if you remove it and decide to add +again), we can do the following: + + cd public; ln -s ../doc + +That's it! Now point your browser at http://localhost:3000/doc and see all the +available documentation listed for you. + +*Note:* doc_browser plugin is activated only in development mode. (i.e. when +ENV['RAILS_ENV] == 'development') + +== Knows limitations + +* tested only with WEBrick + +== Sending Feedback + +Send suggestions, bug reports, patches, pizza and beer to +terceiro@colivre.coop.br + +== License + +doc_browser - a documentation browser plugin for Rails +Copyright (C) 2007 Colivre + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +Please see the file LICENSE (distributed together with the plugin) for +the full terms of distribution. diff --git a/vendor/plugins/doc_browser/Rakefile b/vendor/plugins/doc_browser/Rakefile new file mode 100644 index 0000000..c76141e --- /dev/null +++ b/vendor/plugins/doc_browser/Rakefile @@ -0,0 +1,22 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the doc_browser plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the doc_browser plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'DocBrowser plugin' + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/vendor/plugins/doc_browser/controllers/doc_controller.rb b/vendor/plugins/doc_browser/controllers/doc_controller.rb new file mode 100644 index 0000000..b261cd9 --- /dev/null +++ b/vendor/plugins/doc_browser/controllers/doc_controller.rb @@ -0,0 +1,15 @@ +require 'doc_browser' + +# Controller for serving documentation installed in a Rails application +class DocController < ApplicationController + + self.template_root = File.join(File.dirname(__FILE__), '..', 'views') + + layout 'doc' + + def index + @docs = DocBrowser.find_docs + @errors = DocBrowser.errors + end + +end diff --git a/vendor/plugins/doc_browser/init.rb b/vendor/plugins/doc_browser/init.rb new file mode 100644 index 0000000..1751a42 --- /dev/null +++ b/vendor/plugins/doc_browser/init.rb @@ -0,0 +1,6 @@ +if ENV['RAILS_ENV'] == 'development' + controllers_path = File.join(File.dirname(__FILE__), 'controllers') + $LOAD_PATH << controllers_path + Dependencies.load_paths << controllers_path + config.controller_paths << controllers_path +end diff --git a/vendor/plugins/doc_browser/install.rb b/vendor/plugins/doc_browser/install.rb new file mode 100644 index 0000000..f38ed56 --- /dev/null +++ b/vendor/plugins/doc_browser/install.rb @@ -0,0 +1,11 @@ +# Install hook code here + +#public_dir = File.join(RAILS_ROOT, 'public') +#Dir.chdir(public_dir) do |dir| +# File.symlink('../doc', 'doc') +#end + +#puts "*****************************************" +#puts "A symbolic link do your doc directory was just created." +#puts " doc_browser plugins needs it to work." +#puts "*****************************************" diff --git a/vendor/plugins/doc_browser/lib/doc_browser.rb b/vendor/plugins/doc_browser/lib/doc_browser.rb new file mode 100644 index 0000000..1c04dab --- /dev/null +++ b/vendor/plugins/doc_browser/lib/doc_browser.rb @@ -0,0 +1,83 @@ +# doc_browser - a documentation browser plugin for Rails +# Copyright (C) 2007 Colivre +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Searches for documentation installed in a Rails application. +module DocBrowser + + # searches for documentation installed in a Rails application. Returns an + # Array of Hashes with the found docs. Each entry of the array looks like + # this: + # { + # :name => 'name', + # :title => 'Some descriptive title', + # :link => 'doc/name/index.html', + # :doc_exists => true, # in the case the documentation is installed, + # :dont_exist_message => 'some message' # to be displayed if the doc is not installed + # } + def self.find_docs(root = RAILS_ROOT) + docs = [] + + # API documentation + docs << { + :name => 'api', + :title => 'Rails API documentation', + :link => "/doc/api/index.html", + :doc_exists => File.exists?(File.join(root, 'doc', 'api')), + :dont_exist_message => 'not present. Run rake doc:rails to generate documentation for the Rails API.', + } + + # Application documentation + docs << { + :name => 'app', + :title => 'Application documentation', + :link => "/doc/app/index.html", + :doc_exists => File.exists?(File.join(root, 'doc', 'app')), + :dont_exist_message => 'not present. Run rake doc:app to generate documentation for your application.', + } + + Dir.glob(File.join(root, 'vendor', 'plugins', '*')).select do |f| + File.directory?(f) + end.map do |dir| + name = File.basename(dir) + { + :name => name, + :title => "#{name} plugin", + :link => ("/doc/plugins/#{name}/index.html"), + :doc_exists => File.exists?(File.join(root, 'doc', 'plugins', name)), + :dont_exist_message => 'Documentation not generated. Run rake doc:plugins to generate documentation for all plugins in your application.', + } + end.each do |item| + docs << item + end + + docs + end + + # checks if there are any errors that may prevent the user to see any + # documentation. Returns an Array with error messages. + # + # An empty Array, of course, means no errors. + def self.errors(root = RAILS_ROOT) + errors = [] + + unless File.exists?(File.join(root, 'public', 'doc')) + errors << "There is no symbolic link to your doc directory inside your public directory. Documentation links are probably going to be broken (or even point to parts of your application). To fix this, enter your public/ directory and do ln -s ../doc" + end + errors + end + +end + diff --git a/vendor/plugins/doc_browser/tasks/doc_browser_tasks.rake b/vendor/plugins/doc_browser/tasks/doc_browser_tasks.rake new file mode 100644 index 0000000..be4acc6 --- /dev/null +++ b/vendor/plugins/doc_browser/tasks/doc_browser_tasks.rake @@ -0,0 +1,4 @@ +# desc "Explaining what the task does" +# task :doc_browser do +# # Task goes here +# end \ No newline at end of file diff --git a/vendor/plugins/doc_browser/test/doc_browser_test.rb b/vendor/plugins/doc_browser/test/doc_browser_test.rb new file mode 100644 index 0000000..a548872 --- /dev/null +++ b/vendor/plugins/doc_browser/test/doc_browser_test.rb @@ -0,0 +1,24 @@ +require File.join(File.dirname(__FILE__), 'test_helper') +require 'test/unit' +require File.join(File.dirname(__FILE__), '..', 'lib', 'doc_browser') + +class DocBroserTest < Test::Unit::TestCase + + def roots(name) + File.join(File.dirname(__FILE__), 'fixtures', name) + end + + def test_should_list_existing_docs + docs = DocBrowser.find_docs(roots('regular')) + assert_kind_of Array, docs + assert_equal 3, docs.size + end + + def test_should_detect_missing_symlink + errors = DocBrowser.errors(roots('no_symlink')) + assert(errors.any? do |item| + item =~ /no symbolic link/ + end) + end + +end diff --git a/vendor/plugins/doc_browser/test/doc_controller_test.rb b/vendor/plugins/doc_browser/test/doc_controller_test.rb new file mode 100644 index 0000000..0854dc4 --- /dev/null +++ b/vendor/plugins/doc_browser/test/doc_controller_test.rb @@ -0,0 +1,20 @@ +require File.join(File.dirname(__FILE__), 'test_helper') +require 'test/unit' +require File.join(File.dirname(__FILE__), '..', 'controllers', 'doc_controller') + +class DocController; def rescue_action(e) raise e end; end + +class DocControllerTest < Test::Unit::TestCase + + def setup + @controller = DocController.new + end + + def test_index + @controller.index + assert_kind_of Array, assigns(:docs) + assert_kind_of Array, assigns(:errors) + end + + +end diff --git a/vendor/plugins/doc_browser/test/fixtures/regular/doc/api/index.html b/vendor/plugins/doc_browser/test/fixtures/regular/doc/api/index.html new file mode 100644 index 0000000..c341a40 --- /dev/null +++ b/vendor/plugins/doc_browser/test/fixtures/regular/doc/api/index.html @@ -0,0 +1 @@ + diff --git a/vendor/plugins/doc_browser/test/fixtures/regular/doc/app/index.html b/vendor/plugins/doc_browser/test/fixtures/regular/doc/app/index.html new file mode 100644 index 0000000..c341a40 --- /dev/null +++ b/vendor/plugins/doc_browser/test/fixtures/regular/doc/app/index.html @@ -0,0 +1 @@ + diff --git a/vendor/plugins/doc_browser/test/fixtures/regular/doc/plugins/some_plugin/index.html b/vendor/plugins/doc_browser/test/fixtures/regular/doc/plugins/some_plugin/index.html new file mode 100644 index 0000000..c341a40 --- /dev/null +++ b/vendor/plugins/doc_browser/test/fixtures/regular/doc/plugins/some_plugin/index.html @@ -0,0 +1 @@ + diff --git a/vendor/plugins/doc_browser/test/fixtures/regular/vendor/plugins/some_plugin/init.rb b/vendor/plugins/doc_browser/test/fixtures/regular/vendor/plugins/some_plugin/init.rb new file mode 100644 index 0000000..1bb8bf6 --- /dev/null +++ b/vendor/plugins/doc_browser/test/fixtures/regular/vendor/plugins/some_plugin/init.rb @@ -0,0 +1 @@ +# empty diff --git a/vendor/plugins/doc_browser/test/test_helper.rb b/vendor/plugins/doc_browser/test/test_helper.rb new file mode 100644 index 0000000..ba9edeb --- /dev/null +++ b/vendor/plugins/doc_browser/test/test_helper.rb @@ -0,0 +1,9 @@ +ENV["RAILS_ENV"] = "test" +require File.expand_path(File.dirname(__FILE__) + "/../../../../config/environment") + +class Test::Unit::TestCase + protected + def assigns(sym) + @controller.instance_variable_get("@#{sym}") + end +end diff --git a/vendor/plugins/doc_browser/uninstall.rb b/vendor/plugins/doc_browser/uninstall.rb new file mode 100644 index 0000000..9738333 --- /dev/null +++ b/vendor/plugins/doc_browser/uninstall.rb @@ -0,0 +1 @@ +# Uninstall hook code here diff --git a/vendor/plugins/doc_browser/views/doc/index.rhtml b/vendor/plugins/doc_browser/views/doc/index.rhtml new file mode 100644 index 0000000..1e9772a --- /dev/null +++ b/vendor/plugins/doc_browser/views/doc/index.rhtml @@ -0,0 +1,42 @@ +

+

Rails Documentation Browser

+ A central place for all your Rails documentation +
+ +
+
+
+ +

+ Welcome to the documentation browser. This page lists all the documentation you have generated in your application, including Rails API documentation, documentation for your application, and documentation for the installed plugins. +

+

+ Below you'll have a list of links to all the documentations supported. In the case any documentation is not available you'll be given a hint on how you should generate it. +

+ +<% unless @errors.empty? %> +
+

Errors

+
    + <% @errors.each do |error| %> +
  • <%= error %>
  • + <% end %> +
+
+<% end %> + +
    + <% @docs.each do |item| %> +
  • <%= item[:doc_exists] ? (link_to item[:title], item[:link]) : (content_tag('span', item[:name], :class => 'warning') + ': ' + item[:dont_exist_message]) %>
  • + <% end %> +
+ +

+If you have any suggestions regarding Rails Documentation Browser, please check +the doc_browser plugin documentation above to see how you can send +your suggestions, bug reports, and better yet, patches. +

+ +
+
+
diff --git a/vendor/plugins/doc_browser/views/doc/no_doc_symlink.rhtml b/vendor/plugins/doc_browser/views/doc/no_doc_symlink.rhtml new file mode 100644 index 0000000..e498ad9 --- /dev/null +++ b/vendor/plugins/doc_browser/views/doc/no_doc_symlink.rhtml @@ -0,0 +1,8 @@ +

No doc symlink

+ +

+Please create a symbolic link to your 'doc' directory inside yout public directory. You can do soeething linke this:

+ +
+cd public; ln -s ../doc
+
diff --git a/vendor/plugins/doc_browser/views/layouts/doc.rhtml b/vendor/plugins/doc_browser/views/layouts/doc.rhtml new file mode 100644 index 0000000..6d7c70f --- /dev/null +++ b/vendor/plugins/doc_browser/views/layouts/doc.rhtml @@ -0,0 +1,8 @@ + + + + + + <%= yield %> + + diff --git a/vendor/plugins/rails_rcov/MIT-LICENSE b/vendor/plugins/rails_rcov/MIT-LICENSE new file mode 100644 index 0000000..c61f2c8 --- /dev/null +++ b/vendor/plugins/rails_rcov/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2006 Coda Hale + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/plugins/rails_rcov/README b/vendor/plugins/rails_rcov/README new file mode 100644 index 0000000..eb9d84c --- /dev/null +++ b/vendor/plugins/rails_rcov/README @@ -0,0 +1,86 @@ + = rails_rcov plugin for Rails + +rails_rcov provides easy-to-use Rake tasks to determine the code coverage of +your unit, functional, and integration tests using Mauricio Fernandez's rcov +tool. + +== Installation + +First, install rcov from Mauricio's web site +[http://eigenclass.org/hiki.rb?rcov]. Make sure it's on your system path, so +that typing +rcov+ on the command line actually runs it. THIS PLUGIN DOESN'T DO +ANYTHING BESIDES GENERATE ERRORS UNLESS YOU INSTALL RCOV FIRST. RCOV CONTAINS +ALL THE MAGIC, THIS PLUGIN JUST RUNS IT. + +Second, install this plugin. If your project is source-controlled by Subversion +(which it should be, really), the easiest way to install this is via Rails' +plugin script: + + ./script/plugin install -x http://svn.codahale.com/rails_rcov + +If you're not using Subversion, or if you don't want it adding +svn:externals in your project, remove the -x switch: + + ./script/plugin install http://svn.codahale.com/rails_rcov + +== Usage + +For each test:blah task you have for your Rails project, rails_rcov +adds two more: test:blah:rcov and test:blah:clobber_rcov. + +Running rake test:units:rcov, for example, will run your unit tests +through rcov and write the code coverage reports to +your_rails_app/coverage/units. + +Running test:units:clobber_rcov will erase the generated report for the +unit tests. + +Each rcov task takes two optional parameters: RCOV_PARAMS, whose argument is +passed along to rcov, and SHOW_ONLY, which limits the files displayed in the +report. + +RCOV_PARAMS: + # sort by coverage + rake test:units:rcov RCOV_PARAMS="--sort=coverage" + + # show callsites and hide fully covered files + rake test:units:rcov RCOV_PARAMS="--callsites --only-uncovered" + +Check the rcov documentation for more details. + +SHOW_ONLY is a comma-separated list of the files you'd like to see. Right now +there are four types of files rake_rcov recognizes: models, helpers, +controllers, and lib. These can be abbreviated to their first letters: + + # only show files from app/models + rake test:units:rcov SHOW_ONLY=models + + # only show files from app/helpers and app/controllers + rake test:units:rcov SHOW_ONLY=helpers,controllers + + # only show files from app/helpers and app/controllers, with less typing + rake test:units:rcov SHOW_ONLY=h,c + +Please note that rails_rcov has only been tested with a Bash shell, and any +other environment could well explode in your face. If you're having trouble +getting this to work on Windows, please take the time to figure out what's not +working. Most of the time it boils down to the different ways the Window shell +and the Bash shell escape metacharacters. Play around with the way rcov_rake +escapes data (like on line 73, or 78) and send me a fix. I don't have a working +Windows environment anymore, so leaving it up to me won't solve anything. ;-) + +== Resources + +=== Subversion + +* http://svn.codahale.com/rails_rcov + +=== Blog + +* http://blog.codahale.com + +== Credits + +Written by Coda Hale . Thanks to Nils Franzen for a Win32 +escaping patch. Thanks to Alex Wayne for suggesting how to make SHOW_ONLY not be +useless. \ No newline at end of file diff --git a/vendor/plugins/rails_rcov/tasks/rails_rcov.rake b/vendor/plugins/rails_rcov/tasks/rails_rcov.rake new file mode 100644 index 0000000..2ec23ef --- /dev/null +++ b/vendor/plugins/rails_rcov/tasks/rails_rcov.rake @@ -0,0 +1,154 @@ +# This File Uses Magic +# ==================== +# Here's an example of how this file works. As an example, let's say you typed +# this into your terminal: +# +# $ rake --tasks +# +# The rake executable goes through all the various places .rake files can be, +# accumulates them all, and then runs them. When this file is loaded by Rake, +# it iterates through all the tasks, and for each task named 'test:blah' adds +# test:blah:rcov and test:blah:rcov_clobber. +# +# So you've seen all the tasks, and you type this into your terminal: +# +# $ rake test:units:rcov +# +# Rake does the same thing as above, but it runs the test:units:rcov task, which +# pretty much just does this: +# +# $ ruby [this file] [the test you want to run] [some options] +# +# Now this file is run via the Ruby interpreter, and after glomming up the +# options, it acts just like the Rake executable, with a slight difference: it +# passes all the arguments to rcov, not ruby, so all your unit tests get some +# rcov sweet loving. + +if ARGV.grep(/--run-rake-task=/).empty? + # Define all our Rake tasks + + require 'rake/clean' + require 'rcov/rcovtask' + + def to_rcov_task_sym(s) + s = s.gsub(/(test:)/,'') + s.empty? ? nil : s.intern + end + + def to_rcov_task_name(s) + s = s.gsub(/(test:)/,'') + s =~ /s$/i ? s[0..-2] : s + end + + def new_rcov_task(test_name) + output_dir = "./coverage/#{test_name.gsub('test:','')}" + CLOBBER.include(output_dir) + + # Add a task to run the rcov process + desc "Run all #{to_rcov_task_name(test_name)} tests with Rcov to measure coverage" + task :rcov => [:clobber_rcov] do |t| + run_code = '"' << File.expand_path(__FILE__) << '"' + run_code << " --run-rake-task=#{test_name}" + + params = String.new + if ENV['RCOV_PARAMS'] + params << ENV['RCOV_PARAMS'] + end + + # rake test:units:rcov SHOW_ONLY=models,controllers,lib,helpers + # rake test:units:rcov SHOW_ONLY=m,c,l,h + if ENV['SHOW_ONLY'] + show_only = ENV['SHOW_ONLY'].to_s.split(',').map{|x|x.strip} + if show_only.any? + reg_exp = [] + for show_type in show_only + reg_exp << case show_type + when 'm', 'models' : 'app\/models' + when 'c', 'controllers' : 'app\/controllers' + when 'h', 'helpers' : 'app\/helpers' + when 'l', 'lib' : 'lib' + else + show_type + end + end + reg_exp.map!{ |m| "(#{m})" } + params << " -x \\\"^(?!#{reg_exp.join('|')})\\\"" + end + end + + unless params.empty? + run_code << " --rcov-params=\"#{params}\"" + end + + ruby run_code + end + + # Add a task to clean up after ourselves + desc "Remove Rcov reports for #{to_rcov_task_name(test_name)} tests" + task :clobber_rcov do |t| + rm_r output_dir, :force => true + end + + # Link our clobber task to the main one + task :clobber => [:clobber_rcov] + end + + test_tasks = Rake::Task.tasks.select{ |t| t.comment && t.name =~ /^test/ } + for test_task in test_tasks + namespace :test do + if sym = to_rcov_task_sym(test_task.name) + namespace sym do + new_rcov_task(test_task.name) + end + end + end + end +else + # Load rake tasks, hijack ruby, and redirect the task through rcov + begin + require 'rubygems' + rescue LoadError + # don't force people to use rubygems + end + require 'rake' + + module RcovTestSettings + class << self + attr_accessor :output_dir, :options + def to_params + "-o \"#{@output_dir}\" -T -x \"rubygems/*,rcov*\" --rails #{@options}" + end + end + + # load options and arguments from command line + unless (cmd_line = ARGV.grep(/--rcov-params=/)).empty? + @options = cmd_line.first.gsub(/--rcov-params=/, '') + end + end + + def is_windows? + processor, platform, *rest = RUBY_PLATFORM.split("-") + platform == 'mswin32' + end + + # intercept what Rake *would* be doing with Ruby, and send it through Rcov instead + module RakeFileUtils + alias :ruby_without_rcov :ruby + def ruby(*args, &block) + cmd = (is_windows? ? 'rcov.cmd' : 'rcov') << " #{RcovTestSettings.to_params} #{args}" + status = sh(cmd, {}, &block) + puts "View the full results at " + return status + end + end + + # read the test name and execute it (through Rcov) + unless (cmd_line = ARGV.grep(/--run-rake-task=/)).empty? + test_name = cmd_line.first.gsub(/--run-rake-task=/,'') + ARGV.clear; ARGV << test_name + RcovTestSettings.output_dir = File.expand_path("./coverage/#{test_name.gsub('test:','')}") + Rake.application.run + else + raise "No test to execute!" + end +end -- libgit2 0.21.2