Commit afb05ea7e886577ba57d7a0dbeb0505a9ccdf669

Authored by Daniel Cunha
Committed by Daniela Feitosa
1 parent 909c8e7f

Adding multitenancy support

Developed with Caio SBA <caio@colivre.coop.br>

Available only for PostgreSQL by now

See INSTALL.multitenancy for details

(ActionItem1845)
INSTALL.multitenancy 0 → 100644
@@ -0,0 +1,139 @@ @@ -0,0 +1,139 @@
  1 +== Multitenancy support
  2 +
  3 +Multitenancy refers to a principle in software architecture where a
  4 +single instance of the software runs on a server, serving multiple
  5 +client organizations (tenants). Multitenancy is contrasted with a
  6 +multi-instance architecture where separate software instances (or
  7 +hardware systems) are set up for different client organizations. With
  8 +a multitenant architecture, a software application is designed to
  9 +virtually partition its data and configuration, and each client
  10 +organization works with a customized virtual application instance.
  11 +
  12 +Today this feature is available only for PostgreSQL databases.
  13 +
  14 +This document assumes that you have a fully PostgresSQL default Noosfero
  15 +installation as explained at the INSTALL file.
  16 +
  17 +== Separated data
  18 +
  19 +The items below are separated for each hosted environment:
  20 +
  21 +* Uploaded files
  22 +* Database
  23 +* Ferret index
  24 +* ActiveRecord#cache_key
  25 +* Feed updater
  26 +* Delayed Job Workers
  27 +
  28 +== Database configuration file
  29 +
  30 +The file config/database.yml must follow a structure in order to
  31 +achieve multitenancy support. In this example, we will set 3
  32 +different environments: env1, env2 and env3.
  33 +
  34 +Each "hosted" environment must have an entry like this:
  35 +
  36 +env1_production:
  37 + adapter: postgresql
  38 + encoding: unicode
  39 + database: noosfero
  40 + schema_search_path: public
  41 + username: noosfero
  42 + domains:
  43 + - env1.com
  44 + - env1.org
  45 +
  46 +env2_production:
  47 + adapter: postgresql
  48 + encoding: unicode
  49 + database: noosfero
  50 + schema_search_path: env2
  51 + username: noosfero
  52 + domains:
  53 + - env2.com
  54 + - env2.org
  55 +
  56 +env3_production:
  57 + adapter: postgresql
  58 + encoding: unicode
  59 + database: noosfero
  60 + schema_search_path: env3
  61 + username: noosfero
  62 + domains:
  63 + - env3.com
  64 + - env3.net
  65 +
  66 +The "hosted" environments define, besides the schema_search_path, a
  67 +list of domains that, when accessed, tells which database the
  68 +application should use. Also, the environment name must end with
  69 +'_hosting', where 'hosting' is the name of the hosting environment.
  70 +
  71 +You must also tell the application which is the default environment.
  72 +
  73 +production:
  74 + env1_production
  75 +
  76 +On the example above there are only three hosted environments, but it
  77 +can be more than three. The schemas 'env2' and 'env3' must already
  78 +exist in the same database of the hosting environment. As postgres
  79 +user, you can create them typing:
  80 +
  81 +$ psql database_name -c "CREATE SCHEMA env2 AUTHORIZATION database_user"
  82 +$ psql database_name -c "CREATE SCHEMA env3 AUTHORIZATION database_user"
  83 +
  84 +Replace database_name and database_user above with your stuff.
  85 +
  86 +So, yet on this same example, when a user accesses http://env2.com or
  87 +http://env2.org, the Noosfero application running on production will
  88 +turn the database schema to 'env2'. When the access is from domains
  89 +http://env3.com or http://env3.net, the schema to be loaded will be
  90 +'env3'.
  91 +
  92 +There is an example of this file in config/database.yml.multitenancy
  93 +
  94 +== Preparing the database
  95 +
  96 +Now create the environments:
  97 +
  98 +$ RAILS_ENV=production rake multitenancy:create
  99 +
  100 +This command above will create the hosted environment files equal to
  101 +their hosting environment, here called 'production'.
  102 +
  103 +Run db:schema:load for each other environment:
  104 +
  105 +$ RAILS_ENV=env2_production rake db:schema:load
  106 +$ RAILS_ENV=env3_production rake db:schema:load
  107 +
  108 +Then run the migrations for the hosting environment, and it will
  109 +run for each of its hosted environments:
  110 +
  111 +RAILS_ENV=production rake db:migrate
  112 +
  113 +== Start Noosfero
  114 +
  115 +Run Noosfero init file as root:
  116 +
  117 +# invoke-rc.d noosfero start
  118 +
  119 +== Ferret
  120 +
  121 +It's necessary to run only one instance of ferret_server. Don't worry
  122 +about this, Noosfero initializer had already done this for you.
  123 +
  124 +Build or rebuild the Ferret index by running the following task just
  125 +for your hosting environment, do this as noosfero user:
  126 +
  127 +$ RAILS_ENV=production rake multitenancy:reindex
  128 +
  129 +== Feed updater & Delayed job
  130 +
  131 +Just for your information, a daemon of feed-updater and delayed_job
  132 +must be running for each environment. Noosfero initializer do this,
  133 +relax.
  134 +
  135 +== Uploaded files
  136 +
  137 +When running with PostgreSQL, Noosfero uploads stuff to a folder named
  138 +the same way as the running schema. Inside the upload folder root, for
  139 +example, will be public/image_uploads/env2 and public/image_uploads/env3.
app/controllers/application.rb
@@ -2,6 +2,8 @@ @@ -2,6 +2,8 @@
2 # available in all controllers. 2 # available in all controllers.
3 class ApplicationController < ActionController::Base 3 class ApplicationController < ActionController::Base
4 4
  5 + before_filter :change_pg_schema
  6 +
5 include ApplicationHelper 7 include ApplicationHelper
6 layout :get_layout 8 layout :get_layout
7 def get_layout 9 def get_layout
@@ -96,6 +98,12 @@ class ApplicationController &lt; ActionController::Base @@ -96,6 +98,12 @@ class ApplicationController &lt; ActionController::Base
96 98
97 helper_method :current_person, :current_person 99 helper_method :current_person, :current_person
98 100
  101 + def change_pg_schema
  102 + if Noosfero::MultiTenancy.on? and ActiveRecord::Base.postgresql?
  103 + Noosfero::MultiTenancy.db_by_host = request.host
  104 + end
  105 + end
  106 +
99 protected 107 protected
100 108
101 def user 109 def user
app/controllers/box_organizer_controller.rb
@@ -82,7 +82,7 @@ class BoxOrganizerController &lt; ApplicationController @@ -82,7 +82,7 @@ class BoxOrganizerController &lt; ApplicationController
82 def save 82 def save
83 @block = boxes_holder.blocks.find(params[:id]) 83 @block = boxes_holder.blocks.find(params[:id])
84 @block.update_attributes(params[:block]) 84 @block.update_attributes(params[:block])
85 - expire_timeout_fragment(@block.cache_keys) 85 + expire_timeout_fragment(@block.cache_key)
86 redirect_to :action => 'index' 86 redirect_to :action => 'index'
87 end 87 end
88 88
@@ -93,7 +93,7 @@ class BoxOrganizerController &lt; ApplicationController @@ -93,7 +93,7 @@ class BoxOrganizerController &lt; ApplicationController
93 def remove 93 def remove
94 @block = Block.find(params[:id]) 94 @block = Block.find(params[:id])
95 if @block.destroy 95 if @block.destroy
96 - expire_timeout_fragment(@block.cache_keys) 96 + expire_timeout_fragment(@block.cache_key)
97 redirect_to :action => 'index' 97 redirect_to :action => 'index'
98 else 98 else
99 session[:notice] = _('Failed to remove block') 99 session[:notice] = _('Failed to remove block')
app/helpers/sweeper_helper.rb
@@ -22,7 +22,7 @@ module SweeperHelper @@ -22,7 +22,7 @@ module SweeperHelper
22 22
23 # friends blocks 23 # friends blocks
24 blocks = profile.blocks.select{|b| b.kind_of?(FriendsBlock)} 24 blocks = profile.blocks.select{|b| b.kind_of?(FriendsBlock)}
25 - blocks.map(&:cache_keys).each{|ck|expire_timeout_fragment(ck)} 25 + blocks.map(&:cache_key).each{|ck|expire_timeout_fragment(ck)}
26 end 26 end
27 27
28 def expire_communities(profile) 28 def expire_communities(profile)
@@ -34,13 +34,13 @@ module SweeperHelper @@ -34,13 +34,13 @@ module SweeperHelper
34 34
35 # communities block 35 # communities block
36 blocks = profile.blocks.select{|b| b.kind_of?(CommunitiesBlock)} 36 blocks = profile.blocks.select{|b| b.kind_of?(CommunitiesBlock)}
37 - blocks.map(&:cache_keys).each{|ck|expire_timeout_fragment(ck)} 37 + blocks.map(&:cache_key).each{|ck|expire_timeout_fragment(ck)}
38 end 38 end
39 39
40 def expire_enterprises(profile) 40 def expire_enterprises(profile)
41 # enterprises and favorite enterprises blocks 41 # enterprises and favorite enterprises blocks
42 blocks = profile.blocks.select {|b| [EnterprisesBlock, FavoriteEnterprisesBlock].any?{|klass| b.kind_of?(klass)} } 42 blocks = profile.blocks.select {|b| [EnterprisesBlock, FavoriteEnterprisesBlock].any?{|klass| b.kind_of?(klass)} }
43 - blocks.map(&:cache_keys).each{|ck|expire_timeout_fragment(ck)} 43 + blocks.map(&:cache_key).each{|ck|expire_timeout_fragment(ck)}
44 end 44 end
45 45
46 def expire_profile_index(profile) 46 def expire_profile_index(profile)
app/models/block.rb
@@ -119,10 +119,6 @@ class Block &lt; ActiveRecord::Base @@ -119,10 +119,6 @@ class Block &lt; ActiveRecord::Base
119 true 119 true
120 end 120 end
121 121
122 - def cache_keys  
123 - "block-id-#{id}"  
124 - end  
125 -  
126 def timeout 122 def timeout
127 4.hours 123 4.hours
128 end 124 end
app/models/image.rb
@@ -20,4 +20,6 @@ class Image &lt; ActiveRecord::Base @@ -20,4 +20,6 @@ class Image &lt; ActiveRecord::Base
20 20
21 delay_attachment_fu_thumbnails 21 delay_attachment_fu_thumbnails
22 22
  23 + postgresql_attachment_fu
  24 +
23 end 25 end
app/models/thumbnail.rb
@@ -2,4 +2,6 @@ class Thumbnail &lt; ActiveRecord::Base @@ -2,4 +2,6 @@ class Thumbnail &lt; ActiveRecord::Base
2 has_attachment :storage => :file_system, 2 has_attachment :storage => :file_system,
3 :content_type => :image, :max_size => 5.megabytes 3 :content_type => :image, :max_size => 5.megabytes
4 validates_as_attachment 4 validates_as_attachment
  5 +
  6 + postgresql_attachment_fu
5 end 7 end
app/models/uploaded_file.rb
@@ -52,6 +52,8 @@ class UploadedFile &lt; Article @@ -52,6 +52,8 @@ class UploadedFile &lt; Article
52 52
53 delay_attachment_fu_thumbnails 53 delay_attachment_fu_thumbnails
54 54
  55 + postgresql_attachment_fu
  56 +
55 def self.icon_name(article = nil) 57 def self.icon_name(article = nil)
56 if article 58 if article
57 article.image? ? article.public_filename(:icon) : (article.mime_type ? article.mime_type.gsub(/[\/+.]/, '-') : 'upload-file') 59 article.image? ? article.public_filename(:icon) : (article.mime_type ? article.mime_type.gsub(/[\/+.]/, '-') : 'upload-file')
app/sweepers/article_sweeper.rb
@@ -21,7 +21,7 @@ protected @@ -21,7 +21,7 @@ protected
21 blocks = article.profile.blocks 21 blocks = article.profile.blocks
22 blocks += article.profile.environment.blocks if article.profile.environment 22 blocks += article.profile.environment.blocks if article.profile.environment
23 blocks = blocks.select{|b|[RecentDocumentsBlock, BlogArchivesBlock].any?{|c| b.kind_of?(c)}} 23 blocks = blocks.select{|b|[RecentDocumentsBlock, BlogArchivesBlock].any?{|c| b.kind_of?(c)}}
24 - blocks.map(&:cache_keys).each{|ck|expire_timeout_fragment(ck)} 24 + blocks.map(&:cache_key).each{|ck|expire_timeout_fragment(ck)}
25 env = article.profile.environment 25 env = article.profile.environment
26 if env && (env.portal_community == article.profile) 26 if env && (env.portal_community == article.profile)
27 expire_fragment(env.portal_news_cache_key) 27 expire_fragment(env.portal_news_cache_key)
app/sweepers/friendship_sweeper.rb
@@ -35,7 +35,7 @@ protected @@ -35,7 +35,7 @@ protected
35 end 35 end
36 36
37 blocks = profile.blocks.select{|b| b.kind_of?(FriendsBlock)} 37 blocks = profile.blocks.select{|b| b.kind_of?(FriendsBlock)}
38 - blocks.map(&:cache_keys).each{|ck|expire_timeout_fragment(ck)} 38 + blocks.map(&:cache_key).each{|ck|expire_timeout_fragment(ck)}
39 end 39 end
40 40
41 end 41 end
app/sweepers/profile_sweeper.rb
@@ -23,12 +23,12 @@ protected @@ -23,12 +23,12 @@ protected
23 expire_profile_index(profile) if profile.person? 23 expire_profile_index(profile) if profile.person?
24 24
25 profile.blocks.each do |block| 25 profile.blocks.each do |block|
26 - expire_timeout_fragment(block.cache_keys) 26 + expire_timeout_fragment(block.cache_key)
27 end 27 end
28 end 28 end
29 29
30 def expire_statistics_block_cache(profile) 30 def expire_statistics_block_cache(profile)
31 blocks = profile.environment.blocks.select { |b| b.kind_of?(EnvironmentStatisticsBlock) } 31 blocks = profile.environment.blocks.select { |b| b.kind_of?(EnvironmentStatisticsBlock) }
32 - blocks.map(&:cache_keys).each{|ck|expire_timeout_fragment(ck)} 32 + blocks.map(&:cache_key).each{|ck|expire_timeout_fragment(ck)}
33 end 33 end
34 end 34 end
app/sweepers/role_assignment_sweeper.rb
@@ -25,7 +25,7 @@ protected @@ -25,7 +25,7 @@ protected
25 25
26 profile.blocks_to_expire_cache.each { |block| 26 profile.blocks_to_expire_cache.each { |block|
27 blocks = profile.blocks.select{|b| b.kind_of?(block)} 27 blocks = profile.blocks.select{|b| b.kind_of?(block)}
28 - blocks.map(&:cache_keys).each{|ck|expire_timeout_fragment(ck)} 28 + blocks.map(&:cache_key).each{|ck|expire_timeout_fragment(ck)}
29 } 29 }
30 end 30 end
31 31
app/views/shared/block.rhtml
1 <% if block.cacheable? && use_cache %> 1 <% if block.cacheable? && use_cache %>
2 - <% cache_timeout(block.cache_keys, block.timeout.from_now) do %> 2 + <% cache_timeout(block.cache_key, block.timeout.from_now) do %>
3 <%= display_block_content(block, main_content) %> 3 <%= display_block_content(block, main_content) %>
4 <% end %> 4 <% end %>
5 <% else %> 5 <% else %>
config/database.yml.multitenancy 0 → 100644
@@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
  1 +# Refer to INSTALL.multitenancy for more information on Multitenancy support
  2 +env1_production:
  3 + adapter: postgresql
  4 + encoding: unicode
  5 + database: noosfero
  6 + schema_search_path: public
  7 + username: noosfero
  8 + domains:
  9 + - env1.com
  10 + - env1.org
  11 +
  12 +env2_production:
  13 + adapter: postgresql
  14 + encoding: unicode
  15 + database: noosfero
  16 + schema_search_path: env2
  17 + username: noosfero
  18 + domains:
  19 + - env2.com
  20 + - env2.org
  21 +
  22 +env3_production:
  23 + adapter: postgresql
  24 + encoding: unicode
  25 + database: noosfero
  26 + schema_search_path: env3
  27 + username: noosfero
  28 + domains:
  29 + - env3.com
  30 + - env3.net
  31 +
  32 +production:
  33 + env1_production
config/initializers/postgresql_attachment_fu.rb 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +require 'postgresql_attachment_fu'
lib/acts_as_searchable.rb
@@ -2,11 +2,28 @@ module ActsAsSearchable @@ -2,11 +2,28 @@ module ActsAsSearchable
2 2
3 module ClassMethods 3 module ClassMethods
4 def acts_as_searchable(options = {}) 4 def acts_as_searchable(options = {})
  5 + if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
  6 + options[:additional_fields] ||= {}
  7 + options[:additional_fields] = Hash[*options[:additional_fields].collect{ |v| [v, {}] }.flatten] if options[:additional_fields].is_a?(Array)
  8 + options[:additional_fields].merge!(:schema_name => { :index => :untokenized })
  9 + end
5 acts_as_ferret({ :remote => true }.merge(options)) 10 acts_as_ferret({ :remote => true }.merge(options))
6 extend FindByContents 11 extend FindByContents
  12 + send :include, InstanceMethods
  13 + end
  14 +
  15 + module InstanceMethods
  16 + def schema_name
  17 + ActiveRecord::Base.connection.schema_search_path
  18 + end
7 end 19 end
8 20
9 module FindByContents 21 module FindByContents
  22 +
  23 + def schema_name
  24 + ActiveRecord::Base.connection.schema_search_path
  25 + end
  26 +
10 def find_by_contents(query, ferret_options = {}, db_options = {}) 27 def find_by_contents(query, ferret_options = {}, db_options = {})
11 pg_options = {} 28 pg_options = {}
12 if ferret_options[:page] 29 if ferret_options[:page]
@@ -18,8 +35,9 @@ module ActsAsSearchable @@ -18,8 +35,9 @@ module ActsAsSearchable
18 35
19 ferret_options[:limit] = :all 36 ferret_options[:limit] = :all
20 37
  38 + ferret_query = ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? "+schema_name:\"#{schema_name}\" AND #{query}" : query
21 # FIXME this is a HORRIBLE HACK 39 # FIXME this is a HORRIBLE HACK
22 - ids = find_ids_with_ferret(query, ferret_options)[1][0..8000].map{|r|r[:id].to_i} 40 + ids = find_ids_with_ferret(ferret_query, ferret_options)[1][0..8000].map{|r|r[:id].to_i}
23 41
24 if ids.empty? 42 if ids.empty?
25 ids << -1 43 ids << -1
lib/noosfero/core_ext.rb
1 require 'noosfero/core_ext/string' 1 require 'noosfero/core_ext/string'
2 require 'noosfero/core_ext/integer' 2 require 'noosfero/core_ext/integer'
3 require 'noosfero/core_ext/object' 3 require 'noosfero/core_ext/object'
  4 +require 'noosfero/core_ext/active_record'
lib/noosfero/core_ext/active_record.rb 0 → 100644
@@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
  1 +class ActiveRecord::Base
  2 +
  3 + def self.postgresql?
  4 + ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
  5 + end
  6 +
  7 + alias :meta_cache_key :cache_key
  8 + def cache_key
  9 + key = [Noosfero::VERSION, meta_cache_key]
  10 + key.unshift(ActiveRecord::Base.connection.schema_search_path) if ActiveRecord::Base.postgresql?
  11 + key.join('/')
  12 + end
  13 +
  14 +end
lib/noosfero/multi_tenancy.rb 0 → 100644
@@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
  1 +module Noosfero
  2 + class MultiTenancy
  3 +
  4 + def self.mapping
  5 + @mapping ||= self.load_map
  6 + end
  7 +
  8 + def self.on?
  9 + !self.mapping.blank?
  10 + end
  11 +
  12 + def self.db_by_host=(host)
  13 + ActiveRecord::Base.connection.schema_search_path = self.mapping[host]
  14 + end
  15 +
  16 + private
  17 +
  18 + def self.load_map
  19 + db_file = File.join(RAILS_ROOT, 'config', 'database.yml')
  20 + db_config = YAML.load_file(db_file)
  21 + map = { }
  22 + db_config.each do |env, attr|
  23 + next unless env.match(/_#{RAILS_ENV}$/) and attr['adapter'] =~ /^postgresql$/i
  24 + attr['domains'].each { |d| map[d] = attr['schema_search_path'] }
  25 + end
  26 + map
  27 + end
  28 +
  29 + end
  30 +end
lib/postgresql_attachment_fu.rb 0 → 100644
@@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
  1 +module PostgresqlAttachmentFu
  2 +
  3 + module ClassMethods
  4 + def postgresql_attachment_fu
  5 + send :include, InstanceMethods
  6 + end
  7 + end
  8 +
  9 + module InstanceMethods
  10 + def full_filename(thumbnail = nil)
  11 + file_system_path = (thumbnail ? thumbnail_class : self).attachment_options[:path_prefix].to_s
  12 + file_system_path = File.join(file_system_path, ActiveRecord::Base.connection.schema_search_path) if ActiveRecord::Base.postgresql?
  13 + File.join(RAILS_ROOT, file_system_path, *partitioned_path(thumbnail_name_for(thumbnail)))
  14 + end
  15 + end
  16 +
  17 +end
  18 +
  19 +ActiveRecord::Base.send(:extend, PostgresqlAttachmentFu::ClassMethods)
lib/tasks/multitenancy.rake 0 → 100644
@@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
  1 +namespace :multitenancy do
  2 +
  3 + task :create do
  4 + db_envs = ActiveRecord::Base.configurations.keys.select{ |k| k.match(/_development$|_production$|_test$/) }
  5 + cd File.join(RAILS_ROOT, 'config', 'environments'), :verbose => true
  6 + file_envs = Dir.glob "{*_development.rb,*_prodution.rb,*_test.rb}"
  7 + (db_envs.map{ |e| e + '.rb' } - file_envs).each { |env| ln_s env.split('_').last, env }
  8 + end
  9 +
  10 + task :remove do
  11 + db_envs = ActiveRecord::Base.configurations.keys.select{ |k| k.match(/_development$|_production$|_test$/) }
  12 + cd File.join(RAILS_ROOT, 'config', 'environments'), :verbose => true
  13 + file_envs = Dir.glob "{*_development.rb,*_prodution.rb,*_test.rb}"
  14 + (file_envs - db_envs.map{ |e| e + '.rb' }).each { |env| safe_unlink env }
  15 + end
  16 +
  17 + task :reindex => :environment do
  18 + envs = ActiveRecord::Base.configurations.keys.select{ |k| k.match(/_#{RAILS_ENV}$/) }
  19 + models = [ Profile, Article, Product ]
  20 + envs.each do |e|
  21 + puts "Rebuilding Index for #{e}" if Rake.application.options.trace
  22 + ActiveRecord::Base.connection.schema_search_path = ActiveRecord::Base.configurations[e]['schema_search_path']
  23 + models.each do |m|
  24 + if e == envs[0]
  25 + m.rebuild_index
  26 + puts "Rebuilt index for #{m}" if Rake.application.options.trace
  27 + end
  28 + m.paginated_each(:per_page => 50) { |i| i.ferret_update }
  29 + puts "Reindexed all instances of #{m}" if Rake.application.options.trace
  30 + end
  31 + end
  32 + end
  33 +
  34 +end
  35 +
  36 +namespace :db do
  37 +
  38 + task :migrate_other_environments => :environment do
  39 + envs = ActiveRecord::Base.configurations.keys.select{ |k| k.match(/_#{RAILS_ENV}$/) }
  40 + envs.each do |e|
  41 + puts "*** Migrating #{e}" if Rake.application.options.trace
  42 + system "rake db:migrate RAILS_ENV=#{e}"
  43 + end
  44 + end
  45 + task :migrate => :migrate_other_environments
  46 +
  47 +end
script/feed-updater
@@ -7,6 +7,7 @@ @@ -7,6 +7,7 @@
7 # etc. The actual feed update logic is in FeedUpdater. 7 # etc. The actual feed update logic is in FeedUpdater.
8 8
9 require 'daemons' 9 require 'daemons'
  10 +require 'optparse'
10 11
11 NOOSFERO_ROOT = File.expand_path(File.dirname(__FILE__) + '/../') 12 NOOSFERO_ROOT = File.expand_path(File.dirname(__FILE__) + '/../')
12 13
@@ -15,11 +16,16 @@ options = { @@ -15,11 +16,16 @@ options = {
15 :dir => File.dirname(__FILE__) + '/../tmp/pids', 16 :dir => File.dirname(__FILE__) + '/../tmp/pids',
16 :multiple => false, 17 :multiple => false,
17 :backtrace => true, 18 :backtrace => true,
18 - :monitor => true, 19 + :monitor => false
19 } 20 }
20 21
21 -Daemons.run_proc('feed-updater', options) do 22 +OptionParser.new do |opts|
  23 + opts.on("-i", "--identifier=i", "Id") do |i|
  24 + options[:identifier] = i
  25 + end
  26 +end.parse!(ARGV)
  27 +
  28 +Daemons.run_proc("feed-updater.#{options[:identifier]}", options) do
22 require NOOSFERO_ROOT + '/config/environment' 29 require NOOSFERO_ROOT + '/config/environment'
23 FeedUpdater.new.start 30 FeedUpdater.new.start
24 end 31 end
25 -  
script/production
@@ -23,8 +23,7 @@ do_start() { @@ -23,8 +23,7 @@ do_start() {
23 23
24 clear_cache 24 clear_cache
25 ./script/ferret_server -e $RAILS_ENV start 25 ./script/ferret_server -e $RAILS_ENV start
26 - ./script/feed-updater start  
27 - ./script/delayed_job start 26 + environments_loop
28 mongrel_rails cluster::start 27 mongrel_rails cluster::start
29 } 28 }
30 29
@@ -35,6 +34,20 @@ do_stop() { @@ -35,6 +34,20 @@ do_stop() {
35 ./script/ferret_server -e $RAILS_ENV stop 34 ./script/ferret_server -e $RAILS_ENV stop
36 } 35 }
37 36
  37 +environments_loop() {
  38 + environments=$(find ./config/environments -name *_$RAILS_ENV.rb)
  39 + if [ "$environments" ]; then
  40 + for environment in $environments; do
  41 + env=$(basename $environment | cut -d. -f1)
  42 + RAILS_ENV=$env ./script/delayed_job -i $env start
  43 + RAILS_ENV=$env ./script/feed-updater start -i $env
  44 + done
  45 + else
  46 + ./script/delayed_job start
  47 + ./script/feed-updater start
  48 + fi
  49 +}
  50 +
38 case "$ACTION" in 51 case "$ACTION" in
39 start|stop) 52 start|stop)
40 do_$ACTION 53 do_$ACTION
@@ -58,4 +71,3 @@ case &quot;$ACTION&quot; in @@ -58,4 +71,3 @@ case &quot;$ACTION&quot; in
58 exit 1 71 exit 1
59 ;; 72 ;;
60 esac 73 esac
61 -  
test/functional/application_controller_test.rb
@@ -62,7 +62,7 @@ class ApplicationControllerTest &lt; Test::Unit::TestCase @@ -62,7 +62,7 @@ class ApplicationControllerTest &lt; Test::Unit::TestCase
62 def test_local_files_reference 62 def test_local_files_reference
63 assert_local_files_reference 63 assert_local_files_reference
64 end 64 end
65 - 65 +
66 def test_valid_xhtml 66 def test_valid_xhtml
67 assert_valid_xhtml 67 assert_valid_xhtml
68 end 68 end
@@ -384,7 +384,7 @@ class ApplicationControllerTest &lt; Test::Unit::TestCase @@ -384,7 +384,7 @@ class ApplicationControllerTest &lt; Test::Unit::TestCase
384 uses_host 'other.environment' 384 uses_host 'other.environment'
385 get :index 385 get :index
386 assert_tag :tag => 'div', :attributes => {:id => 'user_menu_ul'} 386 assert_tag :tag => 'div', :attributes => {:id => 'user_menu_ul'}
387 - assert_tag :tag => 'div', :attributes => {:id => 'user_menu_ul'}, 387 + assert_tag :tag => 'div', :attributes => {:id => 'user_menu_ul'},
388 :descendant => {:tag => 'a', :attributes => { :href => 'http://other.environment/adminuser' }}, 388 :descendant => {:tag => 'a', :attributes => { :href => 'http://other.environment/adminuser' }},
389 :descendant => {:tag => 'a', :attributes => { :href => 'http://other.environment/myprofile/adminuser' }}, 389 :descendant => {:tag => 'a', :attributes => { :href => 'http://other.environment/myprofile/adminuser' }},
390 :descendant => {:tag => 'a', :attributes => { :href => '/admin' }} 390 :descendant => {:tag => 'a', :attributes => { :href => '/admin' }}
@@ -441,4 +441,22 @@ class ApplicationControllerTest &lt; Test::Unit::TestCase @@ -441,4 +441,22 @@ class ApplicationControllerTest &lt; Test::Unit::TestCase
441 assert_tag :html, :attributes => { :lang => 'es' } 441 assert_tag :html, :attributes => { :lang => 'es' }
442 end 442 end
443 443
  444 + if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
  445 +
  446 + should 'change postgresql schema' do
  447 + uses_host 'schema1.com'
  448 + Noosfero::MultiTenancy.expects(:on?).returns(true)
  449 + Noosfero::MultiTenancy.expects(:mapping).returns({ 'schema1.com' => 'schema1' })
  450 + exception = assert_raise(ActiveRecord::StatementInvalid) { get :index }
  451 + assert_match /SET search_path TO schema1/, exception.message
  452 + end
  453 +
  454 + should 'not change postgresql schema if multitenancy is off' do
  455 + uses_host 'schema1.com'
  456 + Noosfero::MultiTenancy.stubs(:on?).returns(false)
  457 + Noosfero::MultiTenancy.stubs(:mapping).returns({ 'schema1.com' => 'schema1' })
  458 + assert_nothing_raised(ActiveRecord::StatementInvalid) { get :index }
  459 + end
  460 +
  461 + end
444 end 462 end
test/test_helper.rb
@@ -185,6 +185,32 @@ class Test::Unit::TestCase @@ -185,6 +185,32 @@ class Test::Unit::TestCase
185 end 185 end
186 end 186 end
187 187
  188 + def uses_postgresql(schema_name = 'test_schema')
  189 + adapter = ActiveRecord::Base.connection.class
  190 + adapter.any_instance.stubs(:adapter_name).returns('PostgreSQL')
  191 + adapter.any_instance.stubs(:schema_search_path).returns(schema_name)
  192 + reload_for_ferret
  193 + end
  194 +
  195 + def uses_sqlite
  196 + adapter = ActiveRecord::Base.connection.class
  197 + adapter.any_instance.stubs(:adapter_name).returns('SQLite')
  198 + end
  199 +
  200 + def reload_for_ferret
  201 + ActsAsFerret.send(:remove_const, :DEFAULT_FIELD_OPTIONS)
  202 + load File.join(RAILS_ROOT, 'lib', 'acts_as_searchable.rb')
  203 + load File.join(RAILS_ROOT, 'vendor', 'plugins', 'acts_as_ferret', 'lib', 'acts_as_ferret.rb')
  204 + [Article, Profile, Product].each do |clazz|
  205 + inst_meth = clazz.instance_methods.reject{ |m| m =~ /_to_ferret$/ }
  206 + clazz.stubs(:instance_methods).returns(inst_meth)
  207 + end
  208 + #FIXME Is there a way to avoid this replication from model code?
  209 + Article.acts_as_searchable :additional_fields => [ :comment_data ]
  210 + Profile.acts_as_searchable :additional_fields => [ :extra_data_for_index ]
  211 + Product.acts_as_searchable :fields => [ :name, :description, :category_full_name ]
  212 + end
  213 +
188 end 214 end
189 215
190 module NoosferoTestHelper 216 module NoosferoTestHelper
test/unit/article_test.rb
@@ -1503,4 +1503,27 @@ class ArticleTest &lt; Test::Unit::TestCase @@ -1503,4 +1503,27 @@ class ArticleTest &lt; Test::Unit::TestCase
1503 assert !child.accept_uploads? 1503 assert !child.accept_uploads?
1504 end 1504 end
1505 1505
  1506 + should 'index by schema name when database is postgresql' do
  1507 + uses_postgresql 'schema_one'
  1508 + art1 = Article.create!(:name => 'some thing', :profile_id => @profile.id)
  1509 + assert_equal Article.find_by_contents('thing'), [art1]
  1510 + uses_postgresql 'schema_two'
  1511 + art2 = Article.create!(:name => 'another thing', :profile_id => @profile.id)
  1512 + assert_not_includes Article.find_by_contents('thing'), art1
  1513 + assert_includes Article.find_by_contents('thing'), art2
  1514 + uses_postgresql 'schema_one'
  1515 + assert_includes Article.find_by_contents('thing'), art1
  1516 + assert_not_includes Article.find_by_contents('thing'), art2
  1517 + uses_sqlite
  1518 + end
  1519 +
  1520 + should 'not index by schema name when database is not postgresql' do
  1521 + uses_sqlite
  1522 + art1 = Article.create!(:name => 'some thing', :profile_id => @profile.id)
  1523 + assert_equal Article.find_by_contents('thing'), [art1]
  1524 + art2 = Article.create!(:name => 'another thing', :profile_id => @profile.id)
  1525 + assert_includes Article.find_by_contents('thing'), art1
  1526 + assert_includes Article.find_by_contents('thing'), art2
  1527 + end
  1528 +
1506 end 1529 end
test/unit/block_test.rb
@@ -51,14 +51,6 @@ class BlockTest &lt; Test::Unit::TestCase @@ -51,14 +51,6 @@ class BlockTest &lt; Test::Unit::TestCase
51 assert b.cacheable? 51 assert b.cacheable?
52 end 52 end
53 53
54 - should 'provide chache keys' do  
55 - p = create_user('test_user').person  
56 - box = p.boxes[0]  
57 - b = fast_create(Block, :box_id => box.id)  
58 -  
59 - assert_equal( "block-id-#{b.id}", b.cache_keys)  
60 - end  
61 -  
62 should 'list enabled blocks' do 54 should 'list enabled blocks' do
63 block1 = fast_create(Block, :title => 'test 1') 55 block1 = fast_create(Block, :title => 'test 1')
64 block2 = fast_create(Block, :title => 'test 2', :enabled => false) 56 block2 = fast_create(Block, :title => 'test 2', :enabled => false)
test/unit/image_test.rb
@@ -95,4 +95,21 @@ class ImageTest &lt; Test::Unit::TestCase @@ -95,4 +95,21 @@ class ImageTest &lt; Test::Unit::TestCase
95 end 95 end
96 end 96 end
97 97
  98 + should 'upload to a folder with same name as the schema if database is postgresql' do
  99 + uses_postgresql
  100 + file = Image.create!(:uploaded_data => fixture_file_upload('/files/rails.png', 'image/png'), :owner => profile)
  101 + process_delayed_job_queue
  102 + assert_match(/images\/test_schema\/\d{4}\/\d{4}\/rails.png/, Image.find(file.id).public_filename)
  103 + file.destroy
  104 + uses_sqlite
  105 + end
  106 +
  107 + should 'upload to path prefix folder if database is not postgresql' do
  108 + uses_sqlite
  109 + file = Image.create!(:uploaded_data => fixture_file_upload('/files/rails.png', 'image/png'), :owner => profile)
  110 + process_delayed_job_queue
  111 + assert_match(/images\/\d{4}\/\d{4}\/rails.png/, Image.find(file.id).public_filename)
  112 + file.destroy
  113 + end
  114 +
98 end 115 end
test/unit/product_test.rb
@@ -89,7 +89,7 @@ class ProductTest &lt; Test::Unit::TestCase @@ -89,7 +89,7 @@ class ProductTest &lt; Test::Unit::TestCase
89 89
90 should 'be indexed by category full name' do 90 should 'be indexed by category full name' do
91 p = Product.new(:name => 'a test product', :product_category => @product_category) 91 p = Product.new(:name => 'a test product', :product_category => @product_category)
92 - p.expects(:category_full_name).returns('interesting category') 92 + p.stubs(:category_full_name).returns('interesting category')
93 p.save! 93 p.save!
94 94
95 assert_includes Product.find_by_contents('interesting'), p 95 assert_includes Product.find_by_contents('interesting'), p
@@ -352,4 +352,27 @@ class ProductTest &lt; Test::Unit::TestCase @@ -352,4 +352,27 @@ class ProductTest &lt; Test::Unit::TestCase
352 assert_kind_of Unit, product.build_unit 352 assert_kind_of Unit, product.build_unit
353 end 353 end
354 354
  355 + should 'index by schema name when database is postgresql' do
  356 + uses_postgresql 'schema_one'
  357 + p1 = Product.create!(:name => 'some thing', :product_category => @product_category)
  358 + assert_equal Product.find_by_contents('thing'), [p1]
  359 + uses_postgresql 'schema_two'
  360 + p2 = Product.create!(:name => 'another thing', :product_category => @product_category)
  361 + assert_not_includes Product.find_by_contents('thing'), p1
  362 + assert_includes Product.find_by_contents('thing'), p2
  363 + uses_postgresql 'schema_one'
  364 + assert_includes Product.find_by_contents('thing'), p1
  365 + assert_not_includes Product.find_by_contents('thing'), p2
  366 + uses_sqlite
  367 + end
  368 +
  369 + should 'not index by schema name when database is not postgresql' do
  370 + uses_sqlite
  371 + p1 = Product.create!(:name => 'some thing', :product_category => @product_category)
  372 + assert_equal Product.find_by_contents('thing'), [p1]
  373 + p2 = Product.create!(:name => 'another thing', :product_category => @product_category)
  374 + assert_includes Product.find_by_contents('thing'), p1
  375 + assert_includes Product.find_by_contents('thing'), p2
  376 + end
  377 +
355 end 378 end
test/unit/profile_test.rb
@@ -1668,6 +1668,29 @@ class ProfileTest &lt; Test::Unit::TestCase @@ -1668,6 +1668,29 @@ class ProfileTest &lt; Test::Unit::TestCase
1668 assert_equal 1, community.members_count 1668 assert_equal 1, community.members_count
1669 end 1669 end
1670 1670
  1671 + should 'index by schema name when database is postgresql' do
  1672 + uses_postgresql 'schema_one'
  1673 + p1 = Profile.create!(:name => 'some thing', :identifier => 'some-thing')
  1674 + assert_equal Profile.find_by_contents('thing'), [p1]
  1675 + uses_postgresql 'schema_two'
  1676 + p2 = Profile.create!(:name => 'another thing', :identifier => 'another-thing')
  1677 + assert_not_includes Profile.find_by_contents('thing'), p1
  1678 + assert_includes Profile.find_by_contents('thing'), p2
  1679 + uses_postgresql 'schema_one'
  1680 + assert_includes Profile.find_by_contents('thing'), p1
  1681 + assert_not_includes Profile.find_by_contents('thing'), p2
  1682 + uses_sqlite
  1683 + end
  1684 +
  1685 + should 'not index by schema name when database is not postgresql' do
  1686 + uses_sqlite
  1687 + p1 = Profile.create!(:name => 'some thing', :identifier => 'some-thing')
  1688 + assert_equal Profile.find_by_contents('thing'), [p1]
  1689 + p2 = Profile.create!(:name => 'another thing', :identifier => 'another-thing')
  1690 + assert_includes Profile.find_by_contents('thing'), p1
  1691 + assert_includes Profile.find_by_contents('thing'), p2
  1692 + end
  1693 +
1671 private 1694 private
1672 1695
1673 def assert_invalid_identifier(id) 1696 def assert_invalid_identifier(id)
test/unit/uploaded_file_test.rb
@@ -293,4 +293,36 @@ class UploadedFileTest &lt; Test::Unit::TestCase @@ -293,4 +293,36 @@ class UploadedFileTest &lt; Test::Unit::TestCase
293 assert_equal 'upload-file', UploadedFile.icon_name(f) 293 assert_equal 'upload-file', UploadedFile.icon_name(f)
294 end 294 end
295 295
  296 + should 'upload to a folder with same name as the schema if database is postgresql' do
  297 + uses_postgresql 'image_schema_one'
  298 + file1 = UploadedFile.create!(:uploaded_data => fixture_file_upload('/files/rails.png', 'image/png'), :profile => @profile)
  299 + process_delayed_job_queue
  300 + assert_match(/image_schema_one\/\d{4}\/\d{4}\/rails.png/, UploadedFile.find(file1.id).public_filename)
  301 + uses_postgresql 'image_schema_two'
  302 + file2 = UploadedFile.create!(:uploaded_data => fixture_file_upload('/files/test.txt', 'text/plain'), :profile => @profile)
  303 + assert_match(/image_schema_two\/\d{4}\/\d{4}\/test.txt/, UploadedFile.find(file2.id).public_filename)
  304 + file1.destroy
  305 + file2.destroy
  306 + uses_sqlite
  307 + end
  308 +
  309 + should 'upload to path prefix folder if database is not postgresql' do
  310 + uses_sqlite
  311 + file = UploadedFile.create!(:uploaded_data => fixture_file_upload('/files/test.txt', 'text/plain'), :profile => @profile)
  312 + assert_match(/\/\d{4}\/\d{4}\/test.txt/, UploadedFile.find(file.id).public_filename)
  313 + assert_no_match(/test_schema\/\d{4}\/\d{4}\/test.txt/, UploadedFile.find(file.id).public_filename)
  314 + file.destroy
  315 + end
  316 +
  317 + should 'upload thumbnails to a folder with same name as the schema if database is postgresql' do
  318 + uses_postgresql
  319 + file = UploadedFile.create!(:uploaded_data => fixture_file_upload('/files/rails.png', 'image/png'), :profile => @profile)
  320 + process_delayed_job_queue
  321 + UploadedFile.attachment_options[:thumbnails].each do |suffix, size|
  322 + assert_match(/test_schema\/\d{4}\/\d{4}\/rails_#{suffix}.png/, UploadedFile.find(file.id).public_filename(suffix))
  323 + end
  324 + file.destroy
  325 + uses_sqlite
  326 + end
  327 +
296 end 328 end
vendor/plugins/acts_as_ferret/lib/acts_as_ferret.rb
@@ -204,7 +204,7 @@ module ActsAsFerret @@ -204,7 +204,7 @@ module ActsAsFerret
204 # these properties are somewhat vital to the plugin and shouldn't 204 # these properties are somewhat vital to the plugin and shouldn't
205 # be overwritten by the user: 205 # be overwritten by the user:
206 index_definition[:ferret].update( 206 index_definition[:ferret].update(
207 - :key => [:id, :class_name], 207 + :key => (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? [:id, :class_name, :schema_name] : [:id, :class_name]),
208 :path => index_definition[:index_dir], 208 :path => index_definition[:index_dir],
209 :auto_flush => true, # slower but more secure in terms of locking problems TODO disable when running in drb mode? 209 :auto_flush => true, # slower but more secure in terms of locking problems TODO disable when running in drb mode?
210 :create_if_missing => true 210 :create_if_missing => true