My use case is, if the user have role :agency, he can see clients. But I have to use the link between the client's and agency to verify this. Take a look bellow to see my code:
class Agency < ApplicationRecord
has_many :agency_clients
has_many :clients, through: :agency_clients
resourcify
end
class AgencyClient < ActiveRecord::Base
belongs_to :agency
belongs_to :client
end
class Client < ApplicationRecord
has_many :agency_clients
has_many :agencies, through: :agency_clients
resourcify
end
class ClientPolicy < ApplicationPolicy
def show?
user.has_role?(:admin) || user.has_role?(:client, record)
end
class Scope < Scope
def resolve
if user.has_role? :admin
scope.all
elsif user.has_role? :client, :any
scope.with_role(:client, user)
else
scope.none
end
end
end
end
Thanks!
This way solved my problem. I hope it helps others.
My model Agency has many Clients:
class Agency < ApplicationRecord
has_many :clients
resourcify
end
My User have relationships:
class User < ApplicationRecord
has_many :users_roles
has_many :roles, through: :users_roles
has_many :agencies, through: :roles, source: :resource, source_type: 'Agency'
rolify
...
end
Need create the UsersRole model:
class UsersRole < ApplicationRecord
belongs_to :user
belongs_to :role
end
Finally, my ClientPolicy:
class ClientPolicy < ApplicationPolicy
def show?
user.has_role?(:admin) || user.has_role?(:client, record)
end
class Scope < Scope
def resolve
if user.has_role? :admin
scope.all
elsif user.has_role? :client, :any
scope.with_role(:client, user)
elsif user.has_role? :agency, :any
scope.where(agency_id: user.agencies.pluck(:id))
else
scope.none
end
end
end
end
Related
I would like to know if there is a more elegant way to chain has_many through relationships. In my example I have a user whom can have multiple roles. Each role has multiple permissions. So a user has multiple permissions. The code below works fine, but I am wonder if there is a better way to do this.
class User < ActiveRecord::Base
has_many :role_user_mappings
has_many :roles, through: :role_user_mappings
def permissions
permitted_actions = []
self.roles.each do |role|
role.permissions.each do |permission|
permitted_actions << permission
end
end
permitted_actions
end
end
class Role < ActiveRecord::Base
has_many :permission_role_mappings
has_many :permissions, through: :permission_role_mappings
end
class Permission < ActiveRecord::Base
end
class PermissionRoleMapping < ActiveRecord::Base
belongs_to :permission
belongs_to :role
end
class RoleUserMapping < ActiveRecord::Base
belongs_to :user
belongs_to :role
end
I would like to be able to do this.
user.permissions
EDIT: Tried
On thing that I tried that at least DRYs the User model a little bit is adding the function as a concern
module Permittable
extend ActiveSupport::Concern
def permissions
permitted_actions = []
self.roles.each do |role|
role.permissions.each do |permission|
permitted_actions << permission
end
end
permitted_actions
end
end
Have you tried..
class User < ActiveRecord::Base
has_many :role_user_mappings
has_many :roles, through: :role_user_mappings
has_many :permissions, through: roles
That should give you
user.permissions
I'm not sure when the HMT via HMT feature was made available, I know it was missing in earlier versions of rails, but it works for me on Rails 5.
If you do this:
class Permission < ActiveRecord::Base
has_many :permission_role_mappings
end
Then you should be able to do this:
class User < ActiveRecord::Base
has_many :role_user_mappings
has_many :roles, through: :role_user_mappings
def permissions
Permission.
joins(:permission_role_mappings).
where(permission_role_mappings: {role: roles})
end
end
By the way, you may already know this and it may be why you're asking the question... but this is going to give you an N+1 query:
permitted_actions = []
self.roles.each do |role|
role.permissions.each do |permission|
permitted_actions << permission
end
end
permitted_actions
Also, FWIW, when wanting an array back from a collection, you don't need to do:
permitted_actions = []
self.roles.each do |role|
...
end
permitted_actions
You can just do:
roles.map do |role|
...
end
Since map returns an array.
Thanks for reading!
I'm currently working on my new app and searching for the best way to implement next feature
By scenario I need to implement "As a user a have role in the location"
WHAT I HAVE DONE:
Scenario:
When user adds new location to the profile
one of the requred fields is "role". That could be "guest", "manager" or "seller". What's the best way to accomplish his in the model side?
I accomplished this with has_many_through assosiation
CONTROLLER:
def create
#location = Location.new(location_params)
#location.profiles << current_user.profile
#set user role
current_user.profile.profile_location_throughs.where(location_id: location.id).set_role(params[:location][:role])
respond_to do |format|
if #location.save
....
end
end
end
MODELS:
class Profile < ActiveRecord::Base do
has_many :profile_location_throughs
has_many :locations, through: :profile_location_throughs
end
class Location < ActiveRecord::Base do
has_many :profile_location_throughs
has_many :locations, through: :profile_location_throughs
end
class ProfileLocationThrough < ActiveRecord::Base
# with boolean fields: manager, seller, guest
belongs_to :location
belongs_to :profile
def set_role(role)
case role
when "guest"
self.guest = true
when "seller"
self.seller = true
when "manager"
self.manager = true
end
end
end
=====
QUESTION:
Could you suggest more beatiful way to implement his feature?
There are several ways to do role based authorization.
The simplest way is by adding a enum to the users themselves:
class Profile < ApplicationRecord
enum role: [:guest, :seller, :manager]
end
This is pretty limited though as it only allows "global" roles.
If you want resource scoped roles you need a join table.
class Profile < ApplicationRecord
has_many :roles
has_many :locations, through: :roles
def has_role?(role, location = nil)
self.roles.exists?( { name: role, location: location}.compact )
end
def add_role(role, location)
self.roles.create!( { name: role, location: location } )
end
end
class Role < ApplicationRecord
belongs_to :profile
belongs_to :location
end
class Location < ApplicationRecord
has_many :roles
has_many :profiles, through: :roles
end
In this example we are simply using a string for the roles.name column. You could also use an enum if the kinds of roles are limited. If you want to use the same Role model (no pun intended) to scope roles on different kinds of resources you can use a polymorphic belongs_to relationship.
class Role < ApplicationRecord
belongs_to :profile
belongs_to :resource, polymorphic: true
end
class Location < ApplicationRecord
has_many :roles, as: :resource
has_many :profiles, through: :roles
end
class OtherThing < ApplicationRecord
has_many :roles, as: :resource
has_many :profiles, through: :roles
end
Note that roles are just one part of an authentication solution. You would combine this with a authorization lib such as Pundit or CanCanCan which defines the rules about what role gets to do what and enforces those rules.
Rolify - Role management library with resource scoping
I am currently struggling with a has_many :through association in my project.
This is my model
class Group < ActiveRecord::Base
has_many :user_groups ,dependent: :destroy
has_many :users , through: :user_groups
end
class User < ActiveRecord::Base
has_many :user_groups ,dependent: :destroy
has_many :groups , through: :user_groups
end
class UserGroup < ActiveRecord::Base
belongs_to :user , inverse_of: :placements
belongs_to :group , inverse_of: :placements
validates :level , presence: true
end
So when i tried to create new group but it didn't work out.
This is my controller
class GroupController < ApplicationController
def create
group = Group.new(group_params)
group.users << User.find_by(id: current_user.id)
if group.save
render json: group, status: 201, location: [group]
else
render json: { errors: group.errors }, status: 422
end
end
private
def group_params
params.require(:group).permit(:name, :shuttle_price, :court_price)
end
end
But when i call create method i got this error.
Could not find the inverse association for group (:placements in Group)
On this line
group.users << User.find_by(id: 6)
So how can i fix this?
Thanks!
Remove :inverse_of
class UserGroup < ActiveRecord::Base
belongs_to :user
belongs_to :group
validates :level , presence: true
end
You don't need to add inverse_of there. read this when to use inverse_of
I have a following system and I would like to restrict the users controller action 'follow' if the params[:id] is the same as the current user.
I use cancancan (an up to date cancan gem) to do my authorizations work.
controllers/users_controller.rb
def follow
Followership.create(leader_id: params[:id], follower_id: current_user.id)
...
end
models/user.rb
class User < ActiveRecord::Base
has_many :followers, :class_name => 'Followership', dependent: :destroy
has_many :followed_by, :class_name => 'Followership', dependent: :destroy
...
end
models/followership.rb
class Followership < ActiveRecord::Base
belongs_to :leader, :class_name => 'User'
belongs_to :follower, :class_name => 'User'
...
end
Add a validation on your Followship model:
class Followership < ActiveRecord::Base
belongs_to :leader, :class_name => 'User'
belongs_to :follower, :class_name => 'User'
validate :doesnt_follow_self
private
def doesnt_follow_self
errors.add(:base, 'You can\'t follow yourself') if leader == follower
end
end
Perhaps you can use a validation:
#app/models/followership.rb
Class FollowerShip < ActiveRecord::Base
include ActiveModel::Validations
...
validates_with FollowValidator
end
#app/validators/follow_validator.rb
class FollowValidator < ActiveModel::Validator
def validate(record)
if record.leader_id == record.follower_id
record.errors[:leader_id] << "Sorry, you can't follow yourself!"
end
end
end
I was half-way through writing this when #BroiStatse posted
I've got two models
class Payment < ActiveRecord::Base
has_and_belongs_to_many :invoices
after_save :update_invoices_state
def update_invoices_state
self.invoices.each{|i| i.update_state }
end
end
class Invoice < ActiveRecord::Base
has_and_belongs_to_many :payments
def pending_value
paid_value = Money.new(0,self.currency)
self.payments.each{|payment| paid_value += payment.value}
self.value - paid_value
end
def update_state
if self.pending_value.cents >= 0
if self.due_on >= Time.zone.today
new_state = :past_due_date
else
new_state = :pending
end
else
new_state = :paid
end
self.update_attribute :state, new_state
end
end
I've been debuggin this and I've found that when invoice.update_state is run self.payments is empty. Looks like HABTM hasn't been updated yet.
How could i solve this?
I believe HABTM has been mostly replaced by has_many :through.
You would create a join model, something like "InvoicePayment" (or something else creative)
class Payment < ActiveRecord::Base
has_many :invoice_payments
has_many :invoices, :through => :invoicepayments
end
class InvoicePayment < ActiveRecord::Base
belongs_to :invoice
belongs_to :payment
end
class Invoice < ActiveRecord::Base
has_many :invoice_payments
has_many :payments, :through => :invoice_payments
end
This should fix your problem.