The Setup
I'm using Rails 5.2 with the cancancan gem.
rails g scaffold User first_name email:uniq
rails g scaffold Organization name:uniq
rails g scaffold Role name
rails g scaffold Membership user:references organization:references role:refences
user.rb
class User < ApplicationRecord
has_many :memberships
has_many :roles, through: :memberships
has_many :organizations, through: :memberships
end
membership.rb
class Membership < ApplicationRecord
belongs_to :role
belongs_to :organization
belongs_to :user
end
organization.rb
class Organization < ApplicationRecord
has_many :memberships
has_many :users, through: :memberships
end
role.rb
class Role < ApplicationRecord
has_many :memberships
has_many :users, through: :memberships
end
seeds.rb
admin = Role.create(name: 'Admin')
user = Role.create(name: 'User')
abc = Organization.create(name: 'Abc Inc.')
bob = User.create(first_name: 'Bob')
alice = User.create(first_name: 'Alice')
Membership.create(role: user, company: abc, role: user)
Membership.create(role: admin, company: abc, role: admin)
The Task
An admin should be able to manage all users and memberships of the company he/she is admin for. A user can only read all users and memberships of that company.
Here is my take on a cancancan configuration:
ability.rb
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
user_role = Role.find_by_name('User')
admin_role = Role.find_by_name('Admin')
organizations_with_user_role = Organization.includes(:memberships).
where(memberships: {user_id: user.id, role_id: user_role.id})
organizations_with_admin_role = Organization.includes(:memberships).
where(memberships: {user_id: user.id, role_id: admin_role.id})
can :read, Organization, organizations_with_user_role
can :manage, Organization, organizations_with_admin_role
end
end
Then I try to run this code in a view:
<% if can? :read, organization %><%= link_to 'Show', organization %><% end %>
This results with an error page which says:
The can? and cannot? call cannot be used with a raw sql 'can' definition. The checking code cannot be determined for :read #
I guess I'm tackling the problem from a totally wrong angle. How do I have to setup the ability.rb to solve this problem?
I'd use a simple hash of conditions here:
can :read, Organization, memberships: { user_id: user.id, role: { name: 'User' } }
can :manage, Organization, memberships: { user_id: user.id, role: { name: 'Admin' } }
this should be really enough and you have the advantage that, with this syntax, you can also call Organization.accessible_by(ability, :read) and retrieve all the Organizations that a user can read.
From the documentation:
Almost anything that you can pass to a hash of conditions in Active
Record will work here. The only exception is working with model ids.
You can't pass in the model objects directly, you must pass in the
ids.
can :manage, Project, group: { id: user.group_ids }
So try something like:
can :read, Organization, id: organizations_with_user_role.pluck(:id)
On a separate note, why are you using includes instead of joins? Your query can be simplified to (without the need for user_role = Role.find_by_name('User')):
organizations_with_user_role = Organization.joins(memberships: :role).
where(memberships: {user_id: user.id}).where(roles: {name: 'User'})
Related
I have a users table in my db. A user can be either of type 'admin' or 'manager'.
Given the models and schema below, I would like that for each instance of 'manager' user, an 'admin' user could select one, some or all the locations of the tenant that the manager belongs to in order to select which locations the manager can have control over.
My models
class User < ActiveRecord::Base
belongs_to :tenant
class Tenant < ActiveRecord::Base
has_many :users, dependent: :destroy
has_many :locations, dependent: :destroy
class Location < ActiveRecord::Base
belongs_to :tenant, inverse_of: :locations
I've tried two paths
First, trying to establish a scoped has_many association between the User and the Location models. However, I can't wrap my head around structuring this scope so that an 'admin' user could select which locations the 'manager' users can control.
Second, setting up a controlled_locations attribute in the users table. Then I set up some code so that an 'admin' user can select which locations a 'manager' can control, populating its 'controlled_locations' attribute. However, what gets saved in the database (inside the controlled_locations array) is strings instead of instances of locations.
Here's the code that I tried for the second path:
The migration
def change
add_column :users, :controlled_locations, :string, array: true, default: []
end
In the view
= f.input :controlled_locations, label: 'Select', collection: #tenant_locations, include_blank: "Anything", wrapper_html: { class: 'form-group' }, as: :check_boxes, include_hidden: false, input_html: {multiple: true}
In the users controller (inside the update method)
if params["user"]["controlled_locations"]
params["user"]["controlled_locations"].each do |l|
resource.controlled_locations << Location.find(l.to_i)
end
resource.save!
end
What I expect
First of all, I'm not quite sure the second path that I tried is a good approach (storing arrays in the db). So my best choice would be to set up a scoped association if it's possible.
In case the second path is feasible, what I would like to get is something like this. Let's say that logging in an Admin, I selected that the user with ID 1 (a manager) can control one location (Boston Stadium):
user = User.find(1)
user.controlled_locations = [#<Location id: 55, name: "Boston Stadium", created_at: "2018-10-03 12:45:58", updated_at: "2018-10-03 12:45:58", tenant_id: 5>]
Instead, what I get after trying is this:
user = User.find(1)
user.controlled_locations = ["#<Location:0x007fd2be0717a8>"]
Instead of instances of locations, what gets saved in the array is just plain strings.
First, your code is missing the locations association in the Tenant class.
class Tenant < ActiveRecord::Base
has_many :users, dependent: :destroy
has_many :locations
Let's say the variable manager has a User record. Then the locations it can control are:
manager.tenant.locations
If you want, you can shorten this with a delegate statement.
class User < ActiveRecord::Base
belongs_to :tenant
delegate :locations, to: :tenant
then you can call this with
manager.locations
A common pattern used for authorization is roles:
class User < ApplicationRecord
has_many :user_roles
has_many :roles, through: :user_roles
def add_role(name, location)
self.roles << Role.find_or_create_by(name: name, location: location)
end
def has_role?(name, location)
self.roles.exists?(name: name, location: location)
end
end
# rails g model role name:string
# make sure you add a unique index on name and location
class Role < ApplicationRecord
belongs_to :location
has_many :user_roles
has_many :users, through: :user_roles
validates_uniqueness_of :name, scope: :location_id
end
# rails g model user_role user:references role:references
# make sure you add a unique compound index on role_id and user_id
class UserRole < ApplicationRecord
belongs_to :role
belongs_to :user
validates_uniqueness_of :user_id, scope: :role_id
end
class Location < ApplicationRecord
has_many :roles
has_many :users, through: :roles
end
By making the system a bit more generic than say a controlled_locations association you can re-use it for different cases.
Let's say that logging in an Admin, I selected that the user with ID 1
(a manager) can control one location (Boston Stadium)
User.find(1)
.add_role(:manager, Location.find_by(name: "Boston Stadium"))
In actual MVC terms you can do this by setting up roles as a nested resource that can be CRUD'ed just like any other resource. Editing multiple roles in a single form can be done with accepts_nested_attributes or AJAX.
If you want to scope a query by the presence of a role then join the roles and user roles table:
Location.joins(roles: :user_roles)
.where(roles: { name: :manager })
.where(user_roles: { user_id: 1 })
To authenticate a single resource you would do:
class ApplicationController < ActionController::Base
protected
def deny_access
redirect_to "your/sign_in/path", error: 'You are not authorized.'
end
end
class LocationsController < ApplicationController
# ...
def update
#location = Location.find(params[:location_id])
deny_access and return unless current_user.has_role?(:manger, #location)
# ...
end
end
Instead of rolling your own authorization system though I would consider using rolify and pundit.
In my Rails app I have Clients and Users. And Users can have many Clients.
The models are setup as so:
class Client < ApplicationRecord
has_many :client_users, dependent: :destroy
has_many :users, through: :client_users
end
class User < ApplicationRecord
has_many :client_users, dependent: :destroy
has_many :clients, through: :client_users
end
class ClientUser < ApplicationRecord
belongs_to :user
belongs_to :client
end
So if I wanted to create a new client that had the first two users associated with it how would I do it?
e.g.
Client.create!(name: 'Client1', client_users: [User.first, User.second])
Trying that gives me the error:
ActiveRecord::AssociationTypeMismatch: ClientUser(#70142396623360) expected, got #<User id: 1,...
I also want to do this for an RSpec test. e.g.
user1 = create(:user)
user2 = create(:user)
client1 = create(:client, client_users: [user1, user2])
How do I create a client with associated users for in both the Rails console and in an RSpec test?
If you do not want to accept_nested_attributes for anything, as documented here you can also pass block to create.
Client.create!(name: 'Client1') do |client1|
client1.users << [User.find(1), User.find(2), User.find(3)]
end
Try this. It should work
Client.create!(name: 'Client1').client_users.new([{user_id:
User.first},{user_id: User.second}])
You can do this with the following code:
user1 = create(:user)
user2 = create(:user)
client1 = create(:client, users: [user1, user2])
See ClassMethods/has_many for the documentation
collection=objects
Replaces the collections content by deleting and adding objects as
appropriate. If the :through option is true callbacks in the join
models are triggered except destroy callbacks, since deletion is
direct.
If you are using factory_girl you can add trait :with_users like this:
FactoryGirl.define do
factory :client do
trait :with_two_users do
after(:create) do |client|
client.users = create_list :user, 2
end
end
end
end
Now you can create a client with users in test like this:
client = create :client, :with_two_users
accepts_nested_attributes_for :users
and do as so:
Client.create!(name: 'Client1', users_attributes: { ........ })
hope this would work for you.
You can make use of after_create callback
class Client < ApplicationRecord
has_many :client_users, dependent: :destroy
has_many :users, through: :client_users
after_create :add_users
private def add_users
sef.users << [User.first, User.second]
end
end
Alternatively, A simpler approach would be
Client.create!(name: 'Client1', user_ids: [User.first.id, User.second.id])
The reason you're getting a mismatch is because you're specifying the client_users association that expects ClientUser instances, but you're passing in User instances:
# this won't work
Client.create!(client_users: [User.first, User.second])
Instead, since you already specified a users association, you can do this:
Client.create!(users: [User.first, User.second])
There's a simpler way to handle this, though: ditch the join model and use a has_and_belongs_to_many relationship. You still need a clients_users join table in the database, but you don't need a ClientUser model. Rails will handle this automatically under the covers.
class Client < ApplicationRecord
has_and_belongs_to_many :users
end
class User
has_and_belongs_to_many :clients
end
# Any of these work:
client = Client.new(name: "Kung Fu")
user = client.users.new(name: "Panda")
client.users << User.new(name: "Nemo")
client.save # => this will create two users and a client, and add two records to the `clients_users` join table
Just started using STI with a Rails 4 project. Suppose I have User and Blog, and User can share his non-public blogs to some other users as editors or normal viewers.
It doesn't make sense for me to put type column in users table, because in the project, the user is associated with not just blogs, but also things like posts. (The blogs here are more like a platform, and posts are articles. Just an idea here, could be other two things).
So I used another model called BlogUserAssociation to manage the above sharing relationship. Basically this model contains a type column, and I have BlogEditorAssociation and BlogViewerAssociation inherited from it. (Name is a bit clunky.) First question, is this a recommended way to handle the "sharing" situation?
With the above thought, I have:
# blog.rb
class Blog < ActiveRecord::Base
...
has_many :blog_user_associations, dependent: :destroy
has_many :editors, through: :blog_editor_associations, source: :user
has_many :allowed_viewers, through: :blog_viewer_associations, source: :user # STI
...
And
# user.rb
class User < ActiveRecord::Base
...
has_many :blog_user_associations, dependent: :destroy
has_many :editable_blogs, through: :blog_editor_associations, source: :blog
has_many :blogs_shared_for_view, through: :blog_viewer_associations, source: :blog
...
But when I tried to test this with Rspec,
it { should have_many(:editors).through(:blog_editor_associations).source(:user) }
I got the error undefined method 'klass' for nil:NilClass
I believe this is because I didn't say has_many blog_editor_associations in User. But I thought since blog_editor_associations inherits from blog_viewer_associations, I don't have to say has_many again for the sub-model. So is there a reason for not automatically bind has_many to sub-models?
STI seems like overkill for this situation. I prefer to add an attribute to the association model and use scopes to retrieve collections, depending on the value of the attribute. For example, you could name the association model BlogUser, and add a boolean can_edit column. A value of true indicates the user can edit the associated blog.
Then the models look like this:
class Blog < ActiveRecord::Base
has_many :blog_users
has_many :users, through: :blog_users
scope :editable, -> { where(blog_users: {can_edit: true}) }
end
class BlogUser < ActiveRecord::Base
belongs_to :blog
belongs_to :user
end
class User < ActiveRecord::Base
has_many :blog_users
has_many :blogs, through: :blog_users
scope :editors, -> { where(blog_users: {can_edit: true}) }
end
So user.blogs retrieves all blogs associated with the user, and user.blogs.editable retrieves all blogs that the user can edit. blog.users retrieves all users associated with the blog, and blog.users.editors retrieves all users who can edit the blog.
Some tests to demonstrate:
require 'rails_helper'
RSpec.describe User, type: :model do
describe "A user with no associated blogs" do
let(:user) { User.create! }
it "has no blogs" do
expect(user.blogs.empty?).to be true
expect(user.blogs.editable.empty?).to be true
end
end
describe "A user with a non-editable blog association" do
let(:user) { User.create! }
let(:blog) { Blog.create! }
before do
user.blogs << blog
end
it "has one blog" do
expect(user.blogs.count).to eq 1
end
it "has no editable blogs" do
expect(user.blogs.editable.empty?).to be true
end
end
describe "A user with an editable blog association" do
let(:user) { User.create! }
let(:blog) { Blog.create! }
before do
user.blog_users << BlogUser.new(blog: blog, user: user, can_edit: true)
end
it "has one blog" do
expect(user.blogs.count).to eq 1
end
it "has one editable blog" do
expect(user.blogs.editable.count).to eq 1
end
end
end
I have a group model that has_many :members, and has_many :memberships. What I would like to do is make it so that in some groups the creator of the group would make it so that you have to request membership in order to join that specific group. How could I set this up in my rails application?
I have added a boolean field to the memberships ActiveRecord but I dont know how to set it up in a way that would allow me to join groups that dont require the "request a membership" function but also to create a "request a membership" function.
as of right now my models look like this:
class Group < ActiveRecord::Base
belongs_to :creator, :class_name => "User"
has_many :members, :through => :memberships
has_many :memberships, :foreign_key => "new_group_id"
has_many :events
end
class User < ActiveRecord::Base
has_many :groups, foreign_key: :creator_id
has_many :memberships, foreign_key: :member_id
has_many :new_groups, through: :memberships
end
class Membership < ActiveRecord::Base
belongs_to :member, class_name: "User"
belongs_to :new_group, class_name: "Group"
validates :new_group_id, uniqueness: {scope: :member_id}
has_many :accepted_memberships, -> {where(memberships: { approved: true}) }, :through => :memberships
has_many :pending_memberships, -> {where(memberships: { approved: false}) }, :through => :memberships
end
and my membership controller:
class MembershipsController < ApplicationController
def create
#group = Group.find(params[:new_group_id])
#membership = current_user.memberships.build(:new_group_id => params[:new_group_id])
if #membership.save
flash[:notice] = "Joined #{#group.name} "
else
flash[:notice] = "You're already in this group"
end
redirect_to groups_url
end
def destroy
#group = Group.find(params[:id])
#membership = current_user.memberships.find_by(params[membership_id: #group.id]).destroy
redirect_to groups_url
end
end
I believe that you are already very close to your solution, and that it is more of a business problem than a technical one. First I would add a boolean to the group to indicate that approval is required. e.g.
rails generate migration add_require_approval_to_groups require_approval:boolean
This would get set when the creator first creates the group depending upon the type of group that they have created.
Now, somehow a user has to be able to discover that there are groups that they can join, and you need to communicate an awareness to them that for some groups, membership is not automatic, but must be approved by the group creator.
So, assuming that you have communicated this to the user, and that they are on a page with a selection box listing all of the groups that they can become a member of (not necessarily the best design choice, but will do for this example). You need to have a query in your model that will gather all of the available groups that a user can still join.
def self.available_groups(user_id)
where("id not in (select group_id from group_members where user_id = ?)", user_id)
.select("id, name")
.collect{ |g| [g.name, g.id] }
end
In your controller:
#available_groups = Group.available_groups(#current_user)
And in your view:
<h2>Please select the group to join:</h2>
<p>
<%= form_tag :action => 'join_group' do %>
<%= select("group", "id",
#available_groups) %>
<%= submit_tag "Join" %>
<% end %>
</p>
Now, when you process the "post" in your membership_controller, you need to inform the creator that someone is trying to join the group that requires approval (perhaps a mailer). If the require_approval boolean is not set, then you need to automatically approve the user so that they can access the group immediately.
I currently have a model for team.rb and user.rb, which is a many to many relationship. I have created the join table teams_users but I am not sure how to populate this table in my seeds.rb?
For example, I have :
user = User.create({ first_name: 'Kamil', last_name: 'Bo', email: 'bo#gmail.com'})
team = Team.create([{ name: 'Spot Forwards', num_of_games: 10, day_of_play: 4}])
But the following does not work???
TeamsUsers.create({ team_id: team.id, user_id: user.id })
I get a message :
uninitialized constant TeamsUsers
This isn't optimized but
user.team_ids = user.team_ids < team.id
user.save
or if this is the first team
user.team_ids = [team.id]
user.save
ALso start using has_many :through. then you will have a TeamUser model. it's a life saver if the join table needs more attributes
Pick a side to work from and then, as #drhenner suggests, use the _ids property to create the association. For example, working with the User model, create the teams first, then the users, assigning them to teams as you go:
teams = Team.create([
{ name: 'Team 1' },
{ name: 'Team 2' },
{ name: 'Team 3' },
# etc.
])
User.create([
{ name: 'User 1', team_ids: [teams[0].id, teams[2].id] },
{ name: 'User 2', team_ids: [teams[1].id, teams[2].id] },
{ name: 'User 3', team_ids: [teams[0].id, teams[1].id] },
# etc.
])
From comment above:
You can have multiple relationships configured on a has_many :through relationship. It's up to you which ones you want to implement. These are all the possibilities:
class Team < ApplicationRecord
has_many :memberships
has_many :users, through: :memberships
end
class Membership < ApplicationRecord
belongs_to :team
belongs_to :user
end
class User < ApplicationRecord
has_many :memberships
has_many :teams, through: :memberships
end
So, when dealing with the Team model, you can use: team.memberships and team.users;
when dealing with the User model, you can use: user.memberships and user.teams;
and if dealing with the join model, you can use: membership.team and membership.user.
You can omit the relationship references to the join model if you don't use it—especially if you're treating the relationship between Team and User like a standard has_and_belongs_to_many relationship:
class Team < ApplicationRecord
has_many :users, through: :memberships
end
class User < ApplicationRecord
has_many :teams, through: :memberships
end
This gives you team.users and user.teams.
Using Rails, say you have tables foos and bars in a many-to-many relationship using table foos_bars, you can seed their associations like this:
bar1 = Bar.find(1)
bar2 = Bar.find(2)
foo1 = Foo.find(1) # for example
foo1.bars << bar1
foo1.bars << bar2
foo1.save
This will update the joins table foos_bars with associations <foo_id:1, bar_id:1>, <foo_id:1, bar_id:2>
Hope this helps.