Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tenantable_belongs_to method #340

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,15 @@ Project.tasks.all # => all tasks with account_id => 3

Acts_as_tenant uses Rails' `default_scope` method to scope models. Rails 3.1 changed the way `default_scope` works in a good way. A user defined `default_scope` should integrate seamlessly with the one added by `acts_as_tenant`.

### Validating belongs_to associations

If you need to validate that a belongs_to association must have the same tenant as the model is defining the association. You can do so by using:

```ruby
tenantable_belongs_to :association_name
```
All options available to Rails' own `belongs_to` are also available to this method.

### Validating attribute uniqueness

If you need to validate for uniqueness, chances are that you want to scope this validation to a tenant. You can do so by using:
Expand Down
33 changes: 33 additions & 0 deletions lib/acts_as_tenant/model_extensions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,39 @@ def validates_uniqueness_to_tenant(fields, args = {})
validates_uniqueness_of(fields, blank_tenant_validation_args)
end
end

# Creates a `belongs_to` association with an automatic tenant validation.
#
# This method defines a `belongs_to` association where the associated record's tenant ID
# must match the tenant ID of the current record. This ensures that a record can only be associated
# with another record belonging to the same tenant.
#
# ==== Parameters
# * +name+ - The name of the association.
# * +scope+ - An optional scope to be applied to the association (can be +nil+).
# * +options+ - Additional options to be passed to the `belongs_to` method.
#
# ==== Examples
#
# tenantable_belongs_to :project
#
# tenantable_belongs_to :project, -> { where(active: true) }
#
# The validation added ensures that the associated record's tenant ID matches the tenant ID of the
# current record. If they do not match, an error will be added to the model, preventing it from being saved.
#
def tenantable_belongs_to(name, scope = nil, **options)
belongs_to name, scope, **options

fkey = reflect_on_association(ActsAsTenant.tenant_klass).foreign_key.to_sym

validate do
associated_record = send(name)
if associated_record.present? && associated_record.send(fkey) != send(fkey)
errors.add(fkey, "must match the #{name} model's #{fkey}")
end
end
end
end
end
end
8 changes: 8 additions & 0 deletions spec/dummy/app/models/tenantable_task.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class TenantableTask < ActiveRecord::Base
acts_as_tenant :account

tenantable_belongs_to :project
default_scope -> { where(completed: nil).order("name") }

validates_uniqueness_of :name
end
7 changes: 7 additions & 0 deletions spec/dummy/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@
t.column :completed, :boolean
end

create_table :tenantable_tasks, force: true do |t|
t.column :name, :string
t.column :account_id, :integer
t.column :project_id, :integer
t.column :completed, :boolean
end

create_table :countries, force: true do |t|
t.column :name, :string
end
Expand Down
27 changes: 27 additions & 0 deletions spec/models/model_extensions_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -505,4 +505,31 @@
expect(ActsAsTenant.current_tenant).to eq(accounts(:bar))
end
end

describe 'tenantable_belongs_to' do
let(:account) { accounts(:foo) }
let(:other_account) { accounts(:bar) }

it "validates that the task's account_id matches the project's account_id when tenantable_belongs_to is defined" do
project = Project.create!(account: account)
task = TenantableTask.new(project: project, account: account)

expect(task).to be_valid
end

it "is invalid if task's tenant_id does not match the project's tenant_id when tenantable_belongs_to is defined" do
project = Project.create!(account: account)
task = TenantableTask.new(project: project, account: other_account)

expect(task).not_to be_valid
expect(task.errors[:account_id]).to include("must match the project model's account_id")
end

it 'does not enforce tenant_id matching if default belongs_to is used' do
project = Project.create!(account: account)
task = Task.new(project: project, account: other_account)

expect(task).to be_valid
end
end
end