Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

restructuring

  • Loading branch information...
commit 7c1ad503998a0a028500ed4462f24f151f016741 0 parents
@ErwinM authored
4 .gitignore
@@ -0,0 +1,4 @@
+*.gem
+.bundle
+Gemfile.lock
+pkg/*
4 Gemfile
@@ -0,0 +1,4 @@
+source "http://rubygems.org"
+
+# Specify your gem's dependencies in acts_as_tenant.gemspec
+gemspec
20 MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2011 [name of plugin creator]
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 README.rdoc
@@ -0,0 +1,24 @@
+=== Acts As Tenant
+- inspired by, borrows code from
+-
+
+=== Installation
+
+
+=== Getting started
+
+
+=== Configuring Application Controller
+
+
+=== Configuring Models
+* validates uniqueness of limitation
+
+=== Bug reports & suggested improvements
+
+
+=== Maintained by
+
+
+=== License
+Copyright (c) 2011 [name of plugin creator], released under the MIT license
1  Rakefile
@@ -0,0 +1 @@
+require 'bundler/gem_tasks'
20 acts_as_tenant.gemspec
@@ -0,0 +1,20 @@
+# -*- encoding: utf-8 -*-
+$:.push File.expand_path("../lib", __FILE__)
+require "acts_as_tenant/version"
+
+Gem::Specification.new do |s|
+ s.name = "acts_as_tenant"
+ s.version = ActsAsTenant::VERSION
+ s.authors = ["Erwin Matthijssen"]
+ s.email = ["erwin.matthijssen@gmail.com"]
+ s.homepage = "http://www.rollcallnow.com/blog"
+ s.summary = %q{not now}
+ s.description = %q{not now}
+
+ s.rubyforge_project = "acts_as_tenant"
+
+ s.files = `git ls-files`.split("\n")
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+ s.require_paths = ["lib"]
+end
21 lib/acts_as_tenant.rb
@@ -0,0 +1,21 @@
+#RAILS_3 = ::ActiveRecord::VERSION::MAJOR >= 3
+
+require "active_record"
+require "action_controller"
+
+#$LOAD_PATH.unshift(File.dirname(__FILE__))
+
+require "acts_as_tenant"
+require "acts_as_tenant/version"
+require "acts_as_tenant/controller_extensions.rb"
+require "acts_as_tenant/model_extensions.rb"
+
+#$LOAD_PATH.shift
+
+if defined?(ActiveRecord::Base)
+ ActiveRecord::Base.send(:include, ActsAsTenant::ModelExtensions)
+ ActionController::Base.extend ActsAsTenant::ControllerExtensions
+end
+
+
+
46 lib/acts_as_tenant/controller_extensions.rb
@@ -0,0 +1,46 @@
+module ActsAsTenant
+ module ControllerExtensions
+
+ # this method allows setting the current_account by reading the subdomain and looking
+ # it up in the tenant-model passed to the method (defaults to Account). The method will
+ # look for the subdomain in a column referenced by the second argument (defaults to subdomain).
+
+ def set_current_tenant_by_subdomain(tenant = :account, column = :subdomain )
+ self.class_eval do
+ cattr_accessor :tenant_class, :tenant_column
+ attr_accessor :current_tenant
+ end
+
+ self.tenant_class = tenant.to_s.capitalize.constantize
+ self.tenant_column = tenant_column.to_sym
+
+ self.class_eval do
+ before_filter :find_tenant_by_subdomain
+
+ private
+ def find_tenant_by_subdomain
+ ActsAsTenant.current_tenant = tenant_class.where(tenant_column => request.subdomains.first).first
+ @current_tenant_instance = ActsAsTenant.current_tenant
+ end
+ end
+ end
+
+ def set_current_tenant_to(current_tenant_object)
+ self.class_eval do
+ cattr_accessor :tenant_class
+ attr_accessor :current_tenant
+ before_filter lambda { @current_tenant_instance = ActsAsTenant.current_tenant = current_tenant_object }
+ end
+ end
+
+ # helper method to have the current_tenant available in the controller
+ def current_tenant
+ @current_tenant_instance
+ end
+
+ ActiveSupport.on_load(:action_controller) do
+ helper_method :current_tenant
+ end
+
+ end
+end
90 lib/acts_as_tenant/model_extensions.rb
@@ -0,0 +1,90 @@
+# ActsAsTenant
+
+
+module ActsAsTenant
+
+ class << self
+ cattr_accessor :tenant_class
+ attr_accessor :current_tenant
+ end
+
+ module ModelExtensions
+ extend ActiveSupport::Concern
+
+ # Alias the v_uniqueness_of method so we can scope it to the current tenant when relevant
+ included do
+ class << self
+ alias original_validates_uniqueness_of :validates_uniqueness_of unless method_defined?(:original_validates_uniqueness_of)
+ alias validates_uniqueness_of :scoped_validates_uniqueness_of
+ end
+ end
+
+ module ClassMethods
+
+ def acts_as_tenant(association = :account)
+
+ # Method that enables checking if a class is scoped by tenant
+ def scoped_to_tenant?
+ true
+ end
+
+ ActsAsTenant.tenant_class ||= association
+
+ # Setup the association between the class and the tenant class
+ belongs_to association
+
+ # get the tenant model and its foreign key
+ reflection = reflect_on_association association
+ fkey = reflection.foreign_key
+
+
+ # set the current_tenant on newly created objects
+ before_validation Proc.new {|m|
+ return unless ActsAsTenant.current_tenant
+ m.send "#{association}=".to_sym, ActsAsTenant.current_tenant
+ }, :on => :create
+
+ # set the default_scope to scope to current tenant
+ default_scope lambda {
+ where({fkey => ActsAsTenant.current_tenant.id}) if ActsAsTenant.current_tenant
+ }
+
+ # Rewrite accessors to make tenant foreign_key/association immutable
+ define_method "#{fkey}=" do |integer|
+ if new_record?
+ write_attribute(fkey, integer)
+ else
+ raise "#{fkey} is immutable!"
+ end
+ end
+
+ define_method "#{association}=" do |model|
+ if new_record?
+ write_attribute(association, model)
+ else
+ raise "#{association} is immutable!"
+ end
+ end
+
+ # add validation of associations against tenant scope
+ reflect_on_all_associations.each do |a|
+ unless a == reflection || a.macro == :has_many
+ validates_each a.foreign_key.to_sym do |record, attr, value|
+ record.errors.add attr, "is invalid" unless a.name.to_s.classify.constantize.where(:id => value).present?
+ end
+ end
+ end
+ end
+
+ private
+ def scoped_validates_uniqueness_of(fields, args = {})
+ if self.respond_to?(:scoped_to_tenant?) && ActsAsTenant.tenant_class
+ raise "ActsAsTenant: :scope argument of uniqueness validator is not available for classes that are scoped by acts_as_tenant" if args[:scope]
+ args[:scope] = lambda {"#{ActsAsTenant.tenant_class.to_s.downcase}_id"}.call
+ end
+ ret = original_validates_uniqueness_of(fields, args)
+ end
+
+ end
+ end
+end
3  lib/acts_as_tenant/version.rb
@@ -0,0 +1,3 @@
+module ActsAsTenant
+ VERSION = "0.0.1"
+end
2  rails/init.rb
@@ -0,0 +1,2 @@
+ActiveRecord::Base.send(:include, ActsAsTenant::ModelExtensions)
+ActionController::Base.extend ActsAsTenant::ControllerExtensions
BIN  spec/actsastenant.sqlite3
Binary file not shown
3  spec/database.yml
@@ -0,0 +1,3 @@
+sqlite:
+ adapter: sqlite3
+ database: spec/actsastenant.sqlite3
3,253 spec/debug.log
3,253 additions, 0 deletions not shown
155 spec/model_extensions_spec.rb
@@ -0,0 +1,155 @@
+require 'spec_helper'
+
+# Setup the db
+ActiveRecord::Schema.define(:version => 1) do
+ create_table :accounts, :force => true do |t|
+ t.column :name, :string
+ end
+
+ create_table :projects, :force => true do |t|
+ t.column :name, :string
+ t.column :account_id, :integer
+ end
+
+ create_table :tasks, :force => true do |t|
+ t.column :name, :string
+ t.column :account_id, :integer
+ t.column :project_id, :integer
+ t.column :completed, :boolean
+ end
+
+end
+
+# Setup the models
+class Account < ActiveRecord::Base
+ has_many :projects
+end
+
+class Project < ActiveRecord::Base
+ has_many :tasks
+ acts_as_tenant :account
+
+ validates_uniqueness_of :name
+end
+
+class Task < ActiveRecord::Base
+ belongs_to :project
+ default_scope :conditions => { :completed => nil }, :order => "name"
+
+ acts_as_tenant :account
+
+end
+
+# Start testing!
+describe ActsAsTenant do
+ after { ActsAsTenant.current_tenant = nil }
+
+ describe 'Setting the current tenant' do
+ before { ActsAsTenant.current_tenant = :foo }
+ it { ActsAsTenant.current_tenant == :foo }
+ end
+
+
+ describe 'Project.all should be scoped to the current tenant if set' do
+ before do
+ @account1 = Account.create!(:name => 'foo')
+ @account2 = Account.create!(:name => 'bar')
+
+ @project1 = @account1.projects.create!(:name => 'foobar')
+ @project2 = @account2.projects.create!(:name => 'baz')
+
+ ActsAsTenant.current_tenant= @account1
+ @projects = Project.all
+ end
+
+ it { @projects.length.should == 1 }
+ it { @projects.should == [@project1] }
+ end
+
+ describe 'Associations should be correctly scoped by current tenant' do
+ before do
+ @account = Account.create!(:name => 'foo')
+ @project = @account.projects.create!(:name => 'foobar', :account_id => @account.id )
+ # the next line would normally be nearly impossible: a task assigned to a tenant project,
+ # but the task has no tenant assigned
+ @task1 = Task.create!(:name => 'no_tenant', :project => @project)
+
+ ActsAsTenant.current_tenant = @account
+ @task2 = @project.tasks.create!(:name => 'baz')
+ @tasks = @project.tasks
+ end
+
+ it 'should correctly set the tenant on the task created with current_tenant set' do
+ @task2.account.should == @account
+ end
+
+ it 'should filter out the non-tenant task from the project' do
+ @tasks.length.should == 1
+ end
+ end
+
+ describe 'When dealing with a user defined default_scope' do
+ before do
+ @account = Account.create!(:name => 'foo')
+ @project1 = Project.create!(:name => 'inaccessible')
+ @task1 = Task.create!(:name => 'no_tenant', :project => @project1)
+
+ ActsAsTenant.current_tenant = @account
+ @project2 = Project.create!(:name => 'accessible')
+ @task2 = @project2.tasks.create!(:name => 'bar')
+ @task3 = @project2.tasks.create!(:name => 'baz')
+ @task4 = @project2.tasks.create!(:name => 'foo')
+ @task5 = @project2.tasks.create!(:name => 'foobar', :completed => true )
+
+ @tasks= Task.all
+ end
+
+ it 'should apply both the tenant scope and the user defined default_scope, including :order' do
+ @tasks.length.should == 3
+ @tasks.should == [@task2, @task3, @task4]
+ end
+ end
+
+ describe 'tenant_id should be immutable' do
+ before do
+ @account = Account.create!(:name => 'foo')
+ @project = @account.projects.create!(:name => 'bar')
+ end
+
+ it { lambda {@project.account_id = @account.id + 1}.should raise_error }
+ end
+
+ describe 'Associations can only be made with in-scope objects' do
+ before do
+ @account = Account.create!(:name => 'foo')
+ @project1 = Project.create!(:name => 'inaccessible_project', :account_id => @account.id + 1)
+
+ ActsAsTenant.current_tenant = @account
+ @project2 = Project.create!(:name => 'accessible_project')
+ @task = @project2.tasks.create!(:name => 'bar')
+ end
+
+ it { @task.update_attributes(:project_id => @project1.id).should == false }
+ end
+
+ describe 'When using validates_uniqueness_of in a model' do
+ before do
+ @account = Account.create!(:name => 'foo')
+ ActsAsTenant.current_tenant = @account
+ @project1 = Project.create!(:name => 'bar')
+ end
+
+ it 'should not be possible to create a duplicate within the same tenant' do
+ @project2 = Project.create(:name => 'bar').valid?.should == false
+ end
+
+ it 'should be possible to create a duplicate outside the tenant scope' do
+ @account = Account.create!(:name => 'baz')
+ ActsAsTenant.current_tenant = @account
+ @project2 = Project.create(:name => 'bar').valid?.should == true
+ end
+
+ end
+
+
+end
24 spec/spec_helper.rb
@@ -0,0 +1,24 @@
+$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
+$LOAD_PATH.unshift(File.dirname(__FILE__))
+require 'rspec'
+require 'active_record'
+require 'action_controller'
+require 'logger'
+
+require 'acts_as_tenant/model_extensions'
+require 'acts_as_tenant/controller_extensions'
+
+ActiveRecord::Base.send(:include, ActsAsTenant::ModelExtensions)
+ActionController::Base.extend ActsAsTenant::ControllerExtensions
+
+config = YAML::load(IO.read(File.join(File.dirname(__FILE__), 'database.yml')))
+ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), "debug.log"))
+ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite'])
+
+# Requires supporting files with custom matchers and macros, etc,
+# in ./support/ and its subdirectories.
+#Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
+
+#RSpec.configure do |config|
+
+#end
Please sign in to comment.
Something went wrong with that request. Please try again.