diff --git a/INSTALL.multitenancy b/INSTALL.multitenancy new file mode 100644 index 0000000..57d748f --- /dev/null +++ b/INSTALL.multitenancy @@ -0,0 +1,139 @@ +== Multitenancy support + +Multitenancy refers to a principle in software architecture where a +single instance of the software runs on a server, serving multiple +client organizations (tenants). Multitenancy is contrasted with a +multi-instance architecture where separate software instances (or +hardware systems) are set up for different client organizations. With +a multitenant architecture, a software application is designed to +virtually partition its data and configuration, and each client +organization works with a customized virtual application instance. + +Today this feature is available only for PostgreSQL databases. + +This document assumes that you have a fully PostgresSQL default Noosfero +installation as explained at the INSTALL file. + +== Separated data + +The items below are separated for each hosted environment: + +* Uploaded files +* Database +* Ferret index +* ActiveRecord#cache_key +* Feed updater +* Delayed Job Workers + +== Database configuration file + +The file config/database.yml must follow a structure in order to +achieve multitenancy support. In this example, we will set 3 +different environments: env1, env2 and env3. + +Each "hosted" environment must have an entry like this: + +env1_production: + adapter: postgresql + encoding: unicode + database: noosfero + schema_search_path: public + username: noosfero + domains: + - env1.com + - env1.org + +env2_production: + adapter: postgresql + encoding: unicode + database: noosfero + schema_search_path: env2 + username: noosfero + domains: + - env2.com + - env2.org + +env3_production: + adapter: postgresql + encoding: unicode + database: noosfero + schema_search_path: env3 + username: noosfero + domains: + - env3.com + - env3.net + +The "hosted" environments define, besides the schema_search_path, a +list of domains that, when accessed, tells which database the +application should use. Also, the environment name must end with +'_hosting', where 'hosting' is the name of the hosting environment. + +You must also tell the application which is the default environment. + +production: + env1_production + +On the example above there are only three hosted environments, but it +can be more than three. The schemas 'env2' and 'env3' must already +exist in the same database of the hosting environment. As postgres +user, you can create them typing: + +$ psql database_name -c "CREATE SCHEMA env2 AUTHORIZATION database_user" +$ psql database_name -c "CREATE SCHEMA env3 AUTHORIZATION database_user" + +Replace database_name and database_user above with your stuff. + +So, yet on this same example, when a user accesses http://env2.com or +http://env2.org, the Noosfero application running on production will +turn the database schema to 'env2'. When the access is from domains +http://env3.com or http://env3.net, the schema to be loaded will be +'env3'. + +There is an example of this file in config/database.yml.multitenancy + +== Preparing the database + +Now create the environments: + +$ RAILS_ENV=production rake multitenancy:create + +This command above will create the hosted environment files equal to +their hosting environment, here called 'production'. + +Run db:schema:load for each other environment: + +$ RAILS_ENV=env2_production rake db:schema:load +$ RAILS_ENV=env3_production rake db:schema:load + +Then run the migrations for the hosting environment, and it will +run for each of its hosted environments: + +RAILS_ENV=production rake db:migrate + +== Start Noosfero + +Run Noosfero init file as root: + +# invoke-rc.d noosfero start + +== Ferret + +It's necessary to run only one instance of ferret_server. Don't worry +about this, Noosfero initializer had already done this for you. + +Build or rebuild the Ferret index by running the following task just +for your hosting environment, do this as noosfero user: + +$ RAILS_ENV=production rake multitenancy:reindex + +== Feed updater & Delayed job + +Just for your information, a daemon of feed-updater and delayed_job +must be running for each environment. Noosfero initializer do this, +relax. + +== Uploaded files + +When running with PostgreSQL, Noosfero uploads stuff to a folder named +the same way as the running schema. Inside the upload folder root, for +example, will be public/image_uploads/env2 and public/image_uploads/env3. diff --git a/app/controllers/application.rb b/app/controllers/application.rb index cd10b5a..da947e7 100644 --- a/app/controllers/application.rb +++ b/app/controllers/application.rb @@ -2,6 +2,8 @@ # available in all controllers. class ApplicationController < ActionController::Base + before_filter :change_pg_schema + include ApplicationHelper layout :get_layout def get_layout @@ -96,6 +98,12 @@ class ApplicationController < ActionController::Base helper_method :current_person, :current_person + def change_pg_schema + if Noosfero::MultiTenancy.on? and ActiveRecord::Base.postgresql? + Noosfero::MultiTenancy.db_by_host = request.host + end + end + protected def user diff --git a/app/controllers/box_organizer_controller.rb b/app/controllers/box_organizer_controller.rb index 563b5e9..74d9e34 100644 --- a/app/controllers/box_organizer_controller.rb +++ b/app/controllers/box_organizer_controller.rb @@ -82,7 +82,7 @@ class BoxOrganizerController < ApplicationController def save @block = boxes_holder.blocks.find(params[:id]) @block.update_attributes(params[:block]) - expire_timeout_fragment(@block.cache_keys) + expire_timeout_fragment(@block.cache_key) redirect_to :action => 'index' end @@ -93,7 +93,7 @@ class BoxOrganizerController < ApplicationController def remove @block = Block.find(params[:id]) if @block.destroy - expire_timeout_fragment(@block.cache_keys) + expire_timeout_fragment(@block.cache_key) redirect_to :action => 'index' else session[:notice] = _('Failed to remove block') diff --git a/app/helpers/sweeper_helper.rb b/app/helpers/sweeper_helper.rb index 83c3e5d..48a6f9e 100644 --- a/app/helpers/sweeper_helper.rb +++ b/app/helpers/sweeper_helper.rb @@ -22,7 +22,7 @@ module SweeperHelper # friends blocks blocks = profile.blocks.select{|b| b.kind_of?(FriendsBlock)} - blocks.map(&:cache_keys).each{|ck|expire_timeout_fragment(ck)} + blocks.map(&:cache_key).each{|ck|expire_timeout_fragment(ck)} end def expire_communities(profile) @@ -34,13 +34,13 @@ module SweeperHelper # communities block blocks = profile.blocks.select{|b| b.kind_of?(CommunitiesBlock)} - blocks.map(&:cache_keys).each{|ck|expire_timeout_fragment(ck)} + blocks.map(&:cache_key).each{|ck|expire_timeout_fragment(ck)} end def expire_enterprises(profile) # enterprises and favorite enterprises blocks blocks = profile.blocks.select {|b| [EnterprisesBlock, FavoriteEnterprisesBlock].any?{|klass| b.kind_of?(klass)} } - blocks.map(&:cache_keys).each{|ck|expire_timeout_fragment(ck)} + blocks.map(&:cache_key).each{|ck|expire_timeout_fragment(ck)} end def expire_profile_index(profile) diff --git a/app/models/block.rb b/app/models/block.rb index cb5e492..effa633 100644 --- a/app/models/block.rb +++ b/app/models/block.rb @@ -119,10 +119,6 @@ class Block < ActiveRecord::Base true end - def cache_keys - "block-id-#{id}" - end - def timeout 4.hours end diff --git a/app/models/image.rb b/app/models/image.rb index 3390cd2..57ba0eb 100644 --- a/app/models/image.rb +++ b/app/models/image.rb @@ -20,4 +20,6 @@ class Image < ActiveRecord::Base delay_attachment_fu_thumbnails + postgresql_attachment_fu + end diff --git a/app/models/thumbnail.rb b/app/models/thumbnail.rb index ca36658..2b1f523 100644 --- a/app/models/thumbnail.rb +++ b/app/models/thumbnail.rb @@ -2,4 +2,6 @@ class Thumbnail < ActiveRecord::Base has_attachment :storage => :file_system, :content_type => :image, :max_size => 5.megabytes validates_as_attachment + + postgresql_attachment_fu end diff --git a/app/models/uploaded_file.rb b/app/models/uploaded_file.rb index 0ca6341..8b1111f 100644 --- a/app/models/uploaded_file.rb +++ b/app/models/uploaded_file.rb @@ -52,6 +52,8 @@ class UploadedFile < Article delay_attachment_fu_thumbnails + postgresql_attachment_fu + def self.icon_name(article = nil) if article article.image? ? article.public_filename(:icon) : (article.mime_type ? article.mime_type.gsub(/[\/+.]/, '-') : 'upload-file') diff --git a/app/sweepers/article_sweeper.rb b/app/sweepers/article_sweeper.rb index 30b3232..e4955c3 100644 --- a/app/sweepers/article_sweeper.rb +++ b/app/sweepers/article_sweeper.rb @@ -21,7 +21,7 @@ protected blocks = article.profile.blocks blocks += article.profile.environment.blocks if article.profile.environment blocks = blocks.select{|b|[RecentDocumentsBlock, BlogArchivesBlock].any?{|c| b.kind_of?(c)}} - blocks.map(&:cache_keys).each{|ck|expire_timeout_fragment(ck)} + blocks.map(&:cache_key).each{|ck|expire_timeout_fragment(ck)} env = article.profile.environment if env && (env.portal_community == article.profile) expire_fragment(env.portal_news_cache_key) diff --git a/app/sweepers/friendship_sweeper.rb b/app/sweepers/friendship_sweeper.rb index f34de4c..5896261 100644 --- a/app/sweepers/friendship_sweeper.rb +++ b/app/sweepers/friendship_sweeper.rb @@ -35,7 +35,7 @@ protected end blocks = profile.blocks.select{|b| b.kind_of?(FriendsBlock)} - blocks.map(&:cache_keys).each{|ck|expire_timeout_fragment(ck)} + blocks.map(&:cache_key).each{|ck|expire_timeout_fragment(ck)} end end diff --git a/app/sweepers/profile_sweeper.rb b/app/sweepers/profile_sweeper.rb index 0a355e3..c83f02c 100644 --- a/app/sweepers/profile_sweeper.rb +++ b/app/sweepers/profile_sweeper.rb @@ -23,12 +23,12 @@ protected expire_profile_index(profile) if profile.person? profile.blocks.each do |block| - expire_timeout_fragment(block.cache_keys) + expire_timeout_fragment(block.cache_key) end end def expire_statistics_block_cache(profile) blocks = profile.environment.blocks.select { |b| b.kind_of?(EnvironmentStatisticsBlock) } - blocks.map(&:cache_keys).each{|ck|expire_timeout_fragment(ck)} + blocks.map(&:cache_key).each{|ck|expire_timeout_fragment(ck)} end end diff --git a/app/sweepers/role_assignment_sweeper.rb b/app/sweepers/role_assignment_sweeper.rb index 32b47e7..4c700f3 100644 --- a/app/sweepers/role_assignment_sweeper.rb +++ b/app/sweepers/role_assignment_sweeper.rb @@ -25,7 +25,7 @@ protected profile.blocks_to_expire_cache.each { |block| blocks = profile.blocks.select{|b| b.kind_of?(block)} - blocks.map(&:cache_keys).each{|ck|expire_timeout_fragment(ck)} + blocks.map(&:cache_key).each{|ck|expire_timeout_fragment(ck)} } end diff --git a/app/views/shared/block.rhtml b/app/views/shared/block.rhtml index 7ef6fc1..fad4f05 100644 --- a/app/views/shared/block.rhtml +++ b/app/views/shared/block.rhtml @@ -1,5 +1,5 @@ <% if block.cacheable? && use_cache %> - <% cache_timeout(block.cache_keys, block.timeout.from_now) do %> + <% cache_timeout(block.cache_key, block.timeout.from_now) do %> <%= display_block_content(block, main_content) %> <% end %> <% else %> diff --git a/config/database.yml.multitenancy b/config/database.yml.multitenancy new file mode 100644 index 0000000..04c0228 --- /dev/null +++ b/config/database.yml.multitenancy @@ -0,0 +1,33 @@ +# Refer to INSTALL.multitenancy for more information on Multitenancy support +env1_production: + adapter: postgresql + encoding: unicode + database: noosfero + schema_search_path: public + username: noosfero + domains: + - env1.com + - env1.org + +env2_production: + adapter: postgresql + encoding: unicode + database: noosfero + schema_search_path: env2 + username: noosfero + domains: + - env2.com + - env2.org + +env3_production: + adapter: postgresql + encoding: unicode + database: noosfero + schema_search_path: env3 + username: noosfero + domains: + - env3.com + - env3.net + +production: + env1_production diff --git a/config/initializers/postgresql_attachment_fu.rb b/config/initializers/postgresql_attachment_fu.rb new file mode 100644 index 0000000..7068c39 --- /dev/null +++ b/config/initializers/postgresql_attachment_fu.rb @@ -0,0 +1 @@ +require 'postgresql_attachment_fu' diff --git a/lib/acts_as_searchable.rb b/lib/acts_as_searchable.rb index 3bf68b2..7f585e2 100644 --- a/lib/acts_as_searchable.rb +++ b/lib/acts_as_searchable.rb @@ -2,11 +2,28 @@ module ActsAsSearchable module ClassMethods def acts_as_searchable(options = {}) + if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' + options[:additional_fields] ||= {} + options[:additional_fields] = Hash[*options[:additional_fields].collect{ |v| [v, {}] }.flatten] if options[:additional_fields].is_a?(Array) + options[:additional_fields].merge!(:schema_name => { :index => :untokenized }) + end acts_as_ferret({ :remote => true }.merge(options)) extend FindByContents + send :include, InstanceMethods + end + + module InstanceMethods + def schema_name + ActiveRecord::Base.connection.schema_search_path + end end module FindByContents + + def schema_name + ActiveRecord::Base.connection.schema_search_path + end + def find_by_contents(query, ferret_options = {}, db_options = {}) pg_options = {} if ferret_options[:page] @@ -18,8 +35,9 @@ module ActsAsSearchable ferret_options[:limit] = :all + ferret_query = ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? "+schema_name:\"#{schema_name}\" AND #{query}" : query # FIXME this is a HORRIBLE HACK - ids = find_ids_with_ferret(query, ferret_options)[1][0..8000].map{|r|r[:id].to_i} + ids = find_ids_with_ferret(ferret_query, ferret_options)[1][0..8000].map{|r|r[:id].to_i} if ids.empty? ids << -1 diff --git a/lib/noosfero/core_ext.rb b/lib/noosfero/core_ext.rb index 2ee7dbf..aa0459e 100644 --- a/lib/noosfero/core_ext.rb +++ b/lib/noosfero/core_ext.rb @@ -1,3 +1,4 @@ require 'noosfero/core_ext/string' require 'noosfero/core_ext/integer' require 'noosfero/core_ext/object' +require 'noosfero/core_ext/active_record' diff --git a/lib/noosfero/core_ext/active_record.rb b/lib/noosfero/core_ext/active_record.rb new file mode 100644 index 0000000..4935efa --- /dev/null +++ b/lib/noosfero/core_ext/active_record.rb @@ -0,0 +1,14 @@ +class ActiveRecord::Base + + def self.postgresql? + ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' + end + + alias :meta_cache_key :cache_key + def cache_key + key = [Noosfero::VERSION, meta_cache_key] + key.unshift(ActiveRecord::Base.connection.schema_search_path) if ActiveRecord::Base.postgresql? + key.join('/') + end + +end diff --git a/lib/noosfero/multi_tenancy.rb b/lib/noosfero/multi_tenancy.rb new file mode 100644 index 0000000..40a14d2 --- /dev/null +++ b/lib/noosfero/multi_tenancy.rb @@ -0,0 +1,30 @@ +module Noosfero + class MultiTenancy + + def self.mapping + @mapping ||= self.load_map + end + + def self.on? + !self.mapping.blank? + end + + def self.db_by_host=(host) + ActiveRecord::Base.connection.schema_search_path = self.mapping[host] + end + + private + + def self.load_map + db_file = File.join(RAILS_ROOT, 'config', 'database.yml') + db_config = YAML.load_file(db_file) + map = { } + db_config.each do |env, attr| + next unless env.match(/_#{RAILS_ENV}$/) and attr['adapter'] =~ /^postgresql$/i + attr['domains'].each { |d| map[d] = attr['schema_search_path'] } + end + map + end + + end +end diff --git a/lib/postgresql_attachment_fu.rb b/lib/postgresql_attachment_fu.rb new file mode 100644 index 0000000..88e9d6c --- /dev/null +++ b/lib/postgresql_attachment_fu.rb @@ -0,0 +1,19 @@ +module PostgresqlAttachmentFu + + module ClassMethods + def postgresql_attachment_fu + send :include, InstanceMethods + end + end + + module InstanceMethods + def full_filename(thumbnail = nil) + file_system_path = (thumbnail ? thumbnail_class : self).attachment_options[:path_prefix].to_s + file_system_path = File.join(file_system_path, ActiveRecord::Base.connection.schema_search_path) if ActiveRecord::Base.postgresql? + File.join(RAILS_ROOT, file_system_path, *partitioned_path(thumbnail_name_for(thumbnail))) + end + end + +end + +ActiveRecord::Base.send(:extend, PostgresqlAttachmentFu::ClassMethods) diff --git a/lib/tasks/multitenancy.rake b/lib/tasks/multitenancy.rake new file mode 100644 index 0000000..f227ddb --- /dev/null +++ b/lib/tasks/multitenancy.rake @@ -0,0 +1,47 @@ +namespace :multitenancy do + + task :create do + db_envs = ActiveRecord::Base.configurations.keys.select{ |k| k.match(/_development$|_production$|_test$/) } + cd File.join(RAILS_ROOT, 'config', 'environments'), :verbose => true + file_envs = Dir.glob "{*_development.rb,*_prodution.rb,*_test.rb}" + (db_envs.map{ |e| e + '.rb' } - file_envs).each { |env| ln_s env.split('_').last, env } + end + + task :remove do + db_envs = ActiveRecord::Base.configurations.keys.select{ |k| k.match(/_development$|_production$|_test$/) } + cd File.join(RAILS_ROOT, 'config', 'environments'), :verbose => true + file_envs = Dir.glob "{*_development.rb,*_prodution.rb,*_test.rb}" + (file_envs - db_envs.map{ |e| e + '.rb' }).each { |env| safe_unlink env } + end + + task :reindex => :environment do + envs = ActiveRecord::Base.configurations.keys.select{ |k| k.match(/_#{RAILS_ENV}$/) } + models = [ Profile, Article, Product ] + envs.each do |e| + puts "Rebuilding Index for #{e}" if Rake.application.options.trace + ActiveRecord::Base.connection.schema_search_path = ActiveRecord::Base.configurations[e]['schema_search_path'] + models.each do |m| + if e == envs[0] + m.rebuild_index + puts "Rebuilt index for #{m}" if Rake.application.options.trace + end + m.paginated_each(:per_page => 50) { |i| i.ferret_update } + puts "Reindexed all instances of #{m}" if Rake.application.options.trace + end + end + end + +end + +namespace :db do + + task :migrate_other_environments => :environment do + envs = ActiveRecord::Base.configurations.keys.select{ |k| k.match(/_#{RAILS_ENV}$/) } + envs.each do |e| + puts "*** Migrating #{e}" if Rake.application.options.trace + system "rake db:migrate RAILS_ENV=#{e}" + end + end + task :migrate => :migrate_other_environments + +end diff --git a/script/feed-updater b/script/feed-updater index 5ad61a0..3b11137 100755 --- a/script/feed-updater +++ b/script/feed-updater @@ -7,6 +7,7 @@ # etc. The actual feed update logic is in FeedUpdater. require 'daemons' +require 'optparse' NOOSFERO_ROOT = File.expand_path(File.dirname(__FILE__) + '/../') @@ -15,11 +16,16 @@ options = { :dir => File.dirname(__FILE__) + '/../tmp/pids', :multiple => false, :backtrace => true, - :monitor => true, + :monitor => false } -Daemons.run_proc('feed-updater', options) do +OptionParser.new do |opts| + opts.on("-i", "--identifier=i", "Id") do |i| + options[:identifier] = i + end +end.parse!(ARGV) + +Daemons.run_proc("feed-updater.#{options[:identifier]}", options) do require NOOSFERO_ROOT + '/config/environment' FeedUpdater.new.start end - diff --git a/script/production b/script/production index 52bc141..f5f85c3 100755 --- a/script/production +++ b/script/production @@ -23,8 +23,7 @@ do_start() { clear_cache ./script/ferret_server -e $RAILS_ENV start - ./script/feed-updater start - ./script/delayed_job start + environments_loop mongrel_rails cluster::start } @@ -35,6 +34,20 @@ do_stop() { ./script/ferret_server -e $RAILS_ENV stop } +environments_loop() { + environments=$(find ./config/environments -name *_$RAILS_ENV.rb) + if [ "$environments" ]; then + for environment in $environments; do + env=$(basename $environment | cut -d. -f1) + RAILS_ENV=$env ./script/delayed_job -i $env start + RAILS_ENV=$env ./script/feed-updater start -i $env + done + else + ./script/delayed_job start + ./script/feed-updater start + fi +} + case "$ACTION" in start|stop) do_$ACTION @@ -58,4 +71,3 @@ case "$ACTION" in exit 1 ;; esac - diff --git a/test/functional/application_controller_test.rb b/test/functional/application_controller_test.rb index 81af15d..f228ec6 100644 --- a/test/functional/application_controller_test.rb +++ b/test/functional/application_controller_test.rb @@ -62,7 +62,7 @@ class ApplicationControllerTest < Test::Unit::TestCase def test_local_files_reference assert_local_files_reference end - + def test_valid_xhtml assert_valid_xhtml end @@ -384,7 +384,7 @@ class ApplicationControllerTest < Test::Unit::TestCase uses_host 'other.environment' get :index assert_tag :tag => 'div', :attributes => {:id => 'user_menu_ul'} - assert_tag :tag => 'div', :attributes => {:id => 'user_menu_ul'}, + assert_tag :tag => 'div', :attributes => {:id => 'user_menu_ul'}, :descendant => {:tag => 'a', :attributes => { :href => 'http://other.environment/adminuser' }}, :descendant => {:tag => 'a', :attributes => { :href => 'http://other.environment/myprofile/adminuser' }}, :descendant => {:tag => 'a', :attributes => { :href => '/admin' }} @@ -441,4 +441,22 @@ class ApplicationControllerTest < Test::Unit::TestCase assert_tag :html, :attributes => { :lang => 'es' } end + if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' + + should 'change postgresql schema' do + uses_host 'schema1.com' + Noosfero::MultiTenancy.expects(:on?).returns(true) + Noosfero::MultiTenancy.expects(:mapping).returns({ 'schema1.com' => 'schema1' }) + exception = assert_raise(ActiveRecord::StatementInvalid) { get :index } + assert_match /SET search_path TO schema1/, exception.message + end + + should 'not change postgresql schema if multitenancy is off' do + uses_host 'schema1.com' + Noosfero::MultiTenancy.stubs(:on?).returns(false) + Noosfero::MultiTenancy.stubs(:mapping).returns({ 'schema1.com' => 'schema1' }) + assert_nothing_raised(ActiveRecord::StatementInvalid) { get :index } + end + + end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 1e2eea4..fe59ef0 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -185,6 +185,32 @@ class Test::Unit::TestCase end end + def uses_postgresql(schema_name = 'test_schema') + adapter = ActiveRecord::Base.connection.class + adapter.any_instance.stubs(:adapter_name).returns('PostgreSQL') + adapter.any_instance.stubs(:schema_search_path).returns(schema_name) + reload_for_ferret + end + + def uses_sqlite + adapter = ActiveRecord::Base.connection.class + adapter.any_instance.stubs(:adapter_name).returns('SQLite') + end + + def reload_for_ferret + ActsAsFerret.send(:remove_const, :DEFAULT_FIELD_OPTIONS) + load File.join(RAILS_ROOT, 'lib', 'acts_as_searchable.rb') + load File.join(RAILS_ROOT, 'vendor', 'plugins', 'acts_as_ferret', 'lib', 'acts_as_ferret.rb') + [Article, Profile, Product].each do |clazz| + inst_meth = clazz.instance_methods.reject{ |m| m =~ /_to_ferret$/ } + clazz.stubs(:instance_methods).returns(inst_meth) + end + #FIXME Is there a way to avoid this replication from model code? + Article.acts_as_searchable :additional_fields => [ :comment_data ] + Profile.acts_as_searchable :additional_fields => [ :extra_data_for_index ] + Product.acts_as_searchable :fields => [ :name, :description, :category_full_name ] + end + end module NoosferoTestHelper diff --git a/test/unit/article_test.rb b/test/unit/article_test.rb index d531fb3..50fab33 100644 --- a/test/unit/article_test.rb +++ b/test/unit/article_test.rb @@ -1503,4 +1503,27 @@ class ArticleTest < Test::Unit::TestCase assert !child.accept_uploads? end + should 'index by schema name when database is postgresql' do + uses_postgresql 'schema_one' + art1 = Article.create!(:name => 'some thing', :profile_id => @profile.id) + assert_equal Article.find_by_contents('thing'), [art1] + uses_postgresql 'schema_two' + art2 = Article.create!(:name => 'another thing', :profile_id => @profile.id) + assert_not_includes Article.find_by_contents('thing'), art1 + assert_includes Article.find_by_contents('thing'), art2 + uses_postgresql 'schema_one' + assert_includes Article.find_by_contents('thing'), art1 + assert_not_includes Article.find_by_contents('thing'), art2 + uses_sqlite + end + + should 'not index by schema name when database is not postgresql' do + uses_sqlite + art1 = Article.create!(:name => 'some thing', :profile_id => @profile.id) + assert_equal Article.find_by_contents('thing'), [art1] + art2 = Article.create!(:name => 'another thing', :profile_id => @profile.id) + assert_includes Article.find_by_contents('thing'), art1 + assert_includes Article.find_by_contents('thing'), art2 + end + end diff --git a/test/unit/block_test.rb b/test/unit/block_test.rb index a6e859b..e667c63 100644 --- a/test/unit/block_test.rb +++ b/test/unit/block_test.rb @@ -51,14 +51,6 @@ class BlockTest < Test::Unit::TestCase assert b.cacheable? end - should 'provide chache keys' do - p = create_user('test_user').person - box = p.boxes[0] - b = fast_create(Block, :box_id => box.id) - - assert_equal( "block-id-#{b.id}", b.cache_keys) - end - should 'list enabled blocks' do block1 = fast_create(Block, :title => 'test 1') block2 = fast_create(Block, :title => 'test 2', :enabled => false) diff --git a/test/unit/image_test.rb b/test/unit/image_test.rb index 6fbcdb2..bc4175a 100644 --- a/test/unit/image_test.rb +++ b/test/unit/image_test.rb @@ -95,4 +95,21 @@ class ImageTest < Test::Unit::TestCase end end + should 'upload to a folder with same name as the schema if database is postgresql' do + uses_postgresql + file = Image.create!(:uploaded_data => fixture_file_upload('/files/rails.png', 'image/png'), :owner => profile) + process_delayed_job_queue + assert_match(/images\/test_schema\/\d{4}\/\d{4}\/rails.png/, Image.find(file.id).public_filename) + file.destroy + uses_sqlite + end + + should 'upload to path prefix folder if database is not postgresql' do + uses_sqlite + file = Image.create!(:uploaded_data => fixture_file_upload('/files/rails.png', 'image/png'), :owner => profile) + process_delayed_job_queue + assert_match(/images\/\d{4}\/\d{4}\/rails.png/, Image.find(file.id).public_filename) + file.destroy + end + end diff --git a/test/unit/product_test.rb b/test/unit/product_test.rb index de946d4..2562122 100644 --- a/test/unit/product_test.rb +++ b/test/unit/product_test.rb @@ -89,7 +89,7 @@ class ProductTest < Test::Unit::TestCase should 'be indexed by category full name' do p = Product.new(:name => 'a test product', :product_category => @product_category) - p.expects(:category_full_name).returns('interesting category') + p.stubs(:category_full_name).returns('interesting category') p.save! assert_includes Product.find_by_contents('interesting'), p @@ -352,4 +352,27 @@ class ProductTest < Test::Unit::TestCase assert_kind_of Unit, product.build_unit end + should 'index by schema name when database is postgresql' do + uses_postgresql 'schema_one' + p1 = Product.create!(:name => 'some thing', :product_category => @product_category) + assert_equal Product.find_by_contents('thing'), [p1] + uses_postgresql 'schema_two' + p2 = Product.create!(:name => 'another thing', :product_category => @product_category) + assert_not_includes Product.find_by_contents('thing'), p1 + assert_includes Product.find_by_contents('thing'), p2 + uses_postgresql 'schema_one' + assert_includes Product.find_by_contents('thing'), p1 + assert_not_includes Product.find_by_contents('thing'), p2 + uses_sqlite + end + + should 'not index by schema name when database is not postgresql' do + uses_sqlite + p1 = Product.create!(:name => 'some thing', :product_category => @product_category) + assert_equal Product.find_by_contents('thing'), [p1] + p2 = Product.create!(:name => 'another thing', :product_category => @product_category) + assert_includes Product.find_by_contents('thing'), p1 + assert_includes Product.find_by_contents('thing'), p2 + end + end diff --git a/test/unit/profile_test.rb b/test/unit/profile_test.rb index e3a5f00..e5e0c9a 100644 --- a/test/unit/profile_test.rb +++ b/test/unit/profile_test.rb @@ -1668,6 +1668,29 @@ class ProfileTest < Test::Unit::TestCase assert_equal 1, community.members_count end + should 'index by schema name when database is postgresql' do + uses_postgresql 'schema_one' + p1 = Profile.create!(:name => 'some thing', :identifier => 'some-thing') + assert_equal Profile.find_by_contents('thing'), [p1] + uses_postgresql 'schema_two' + p2 = Profile.create!(:name => 'another thing', :identifier => 'another-thing') + assert_not_includes Profile.find_by_contents('thing'), p1 + assert_includes Profile.find_by_contents('thing'), p2 + uses_postgresql 'schema_one' + assert_includes Profile.find_by_contents('thing'), p1 + assert_not_includes Profile.find_by_contents('thing'), p2 + uses_sqlite + end + + should 'not index by schema name when database is not postgresql' do + uses_sqlite + p1 = Profile.create!(:name => 'some thing', :identifier => 'some-thing') + assert_equal Profile.find_by_contents('thing'), [p1] + p2 = Profile.create!(:name => 'another thing', :identifier => 'another-thing') + assert_includes Profile.find_by_contents('thing'), p1 + assert_includes Profile.find_by_contents('thing'), p2 + end + private def assert_invalid_identifier(id) diff --git a/test/unit/uploaded_file_test.rb b/test/unit/uploaded_file_test.rb index 48a07ea..b92517d 100644 --- a/test/unit/uploaded_file_test.rb +++ b/test/unit/uploaded_file_test.rb @@ -293,4 +293,36 @@ class UploadedFileTest < Test::Unit::TestCase assert_equal 'upload-file', UploadedFile.icon_name(f) end + should 'upload to a folder with same name as the schema if database is postgresql' do + uses_postgresql 'image_schema_one' + file1 = UploadedFile.create!(:uploaded_data => fixture_file_upload('/files/rails.png', 'image/png'), :profile => @profile) + process_delayed_job_queue + assert_match(/image_schema_one\/\d{4}\/\d{4}\/rails.png/, UploadedFile.find(file1.id).public_filename) + uses_postgresql 'image_schema_two' + file2 = UploadedFile.create!(:uploaded_data => fixture_file_upload('/files/test.txt', 'text/plain'), :profile => @profile) + assert_match(/image_schema_two\/\d{4}\/\d{4}\/test.txt/, UploadedFile.find(file2.id).public_filename) + file1.destroy + file2.destroy + uses_sqlite + end + + should 'upload to path prefix folder if database is not postgresql' do + uses_sqlite + file = UploadedFile.create!(:uploaded_data => fixture_file_upload('/files/test.txt', 'text/plain'), :profile => @profile) + assert_match(/\/\d{4}\/\d{4}\/test.txt/, UploadedFile.find(file.id).public_filename) + assert_no_match(/test_schema\/\d{4}\/\d{4}\/test.txt/, UploadedFile.find(file.id).public_filename) + file.destroy + end + + should 'upload thumbnails to a folder with same name as the schema if database is postgresql' do + uses_postgresql + file = UploadedFile.create!(:uploaded_data => fixture_file_upload('/files/rails.png', 'image/png'), :profile => @profile) + process_delayed_job_queue + UploadedFile.attachment_options[:thumbnails].each do |suffix, size| + assert_match(/test_schema\/\d{4}\/\d{4}\/rails_#{suffix}.png/, UploadedFile.find(file.id).public_filename(suffix)) + end + file.destroy + uses_sqlite + 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 index f0b03cc..444dda7 100644 --- a/vendor/plugins/acts_as_ferret/lib/acts_as_ferret.rb +++ b/vendor/plugins/acts_as_ferret/lib/acts_as_ferret.rb @@ -204,7 +204,7 @@ module ActsAsFerret # these properties are somewhat vital to the plugin and shouldn't # be overwritten by the user: index_definition[:ferret].update( - :key => [:id, :class_name], + :key => (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? [:id, :class_name, :schema_name] : [:id, :class_name]), :path => index_definition[: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 -- libgit2 0.21.2