Commit afb05ea7e886577ba57d7a0dbeb0505a9ccdf669
Committed by
Daniela Feitosa
1 parent
909c8e7f
Exists in
master
and in
23 other branches
Adding multitenancy support
Developed with Caio SBA <caio@colivre.coop.br> Available only for PostgreSQL by now See INSTALL.multitenancy for details (ActionItem1845)
Showing
32 changed files
with
518 additions
and
34 deletions
Show diff stats
| @@ -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 < ActionController::Base | @@ -96,6 +98,12 @@ class ApplicationController < 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 < ApplicationController | @@ -82,7 +82,7 @@ class BoxOrganizerController < 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 < ApplicationController | @@ -93,7 +93,7 @@ class BoxOrganizerController < 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
app/models/image.rb
app/models/thumbnail.rb
| @@ -2,4 +2,6 @@ class Thumbnail < ActiveRecord::Base | @@ -2,4 +2,6 @@ class Thumbnail < 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 < Article | @@ -52,6 +52,8 @@ class UploadedFile < 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 %> |
| @@ -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 |
| @@ -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
| @@ -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 |
| @@ -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 |
| @@ -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) |
| @@ -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 "$ACTION" in | @@ -58,4 +71,3 @@ case "$ACTION" 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 < Test::Unit::TestCase | @@ -62,7 +62,7 @@ class ApplicationControllerTest < 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 < Test::Unit::TestCase | @@ -384,7 +384,7 @@ class ApplicationControllerTest < 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 < Test::Unit::TestCase | @@ -441,4 +441,22 @@ class ApplicationControllerTest < 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 < Test::Unit::TestCase | @@ -1503,4 +1503,27 @@ class ArticleTest < 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 < Test::Unit::TestCase | @@ -51,14 +51,6 @@ class BlockTest < 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 < Test::Unit::TestCase | @@ -95,4 +95,21 @@ class ImageTest < 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 < Test::Unit::TestCase | @@ -89,7 +89,7 @@ class ProductTest < 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 < Test::Unit::TestCase | @@ -352,4 +352,27 @@ class ProductTest < 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 < Test::Unit::TestCase | @@ -1668,6 +1668,29 @@ class ProfileTest < 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 < Test::Unit::TestCase | @@ -293,4 +293,36 @@ class UploadedFileTest < 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 |