Related
I have 3 models : User, Conversation and ConversationUser
class User < ApplicationRecord
has_many :conversation_users, dependent: :destroy
has_many :conversations, through: :conversation_users
end
class Conversation < ApplicationRecord
has_many :conversation_users, dependent: :destroy
has_many :users, through: :conversation_users
end
class ConversationUser < ApplicationRecord
belongs_to :conversation
belongs_to :user
end
My purpose
I would like to find a Conversation when 2 users have the same conversation (a conversation must only have 2 conversation_users with the id of these 2 users)
For the test
I only created ONE conversation with TWO conversation_users
conversation = Conversation.create
conversation.conversation_users.create(user: User.first)
conversation.conversation_users.create(user: User.second)
My Problem
I want to find the conversation with JUST the first User AND second User.
If I do the following query, Active Record will still show me a conversation because it's doing a OR clause but I need a AND.
The correct result should show me an empty array because there is not a conversation with User.first AND User.third
Conversation.joins(:conversation_users).where(conversation_users: {user_id: [User.first.id, User.third.id]})
User Load (3.3ms) SELECT "users".* FROM "users" ORDER BY "users"."created_at" ASC LIMIT $1 [["LIMIT", 1]]
User Load (2.5ms) SELECT "users".* FROM "users" ORDER BY "users"."created_at" ASC LIMIT $1 OFFSET $2 [["LIMIT", 1], ["OFFSET", 2]]
Conversation Load (3.9ms) SELECT "conversations".* FROM "conversations" INNER JOIN "conversation_users" ON "conversation_users"."conversation_id" = "conversations"."id" WHERE "conversation_users"."user_id" IN ($1, $2) LIMIT $3 [["user_id", "274d2f54-2418-4dec-a696-a5f62196ee85"], ["user_id", "0ef2f797-3518-4096-951b-1837ca28e4e4"], ["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Conversation id: "35556705-a162-4ee4-9776-963ae87bcfa8", created_at: "2020-04-20 09:34:05", updated_at: "2020-04-20 09:34:05">]>
Expectations
Should find a Conversation with User.first and User.second
Should NOT find a Conversation with User.first and User.third
I tried this query for the first expectation but it is returning an empty array
Conversation.joins(:conversation_users).where(conversation_users: {user_id: User.first.id}).where(conversation_users: {user_id: User.second.id})
User Load (3.8ms) SELECT "users".* FROM "users" ORDER BY "users"."created_at" ASC LIMIT $1 [["LIMIT", 1]]
User Load (4.0ms) SELECT "users".* FROM "users" ORDER BY "users"."created_at" ASC LIMIT $1 OFFSET $2 [["LIMIT", 1], ["OFFSET", 1]]
Conversation Load (3.3ms) SELECT "conversations".* FROM "conversations" INNER JOIN "conversation_users" ON "conversation_users"."conversation_id" = "conversations"."id" WHERE "conversation_users"."user_id" = $1 AND "conversation_users"."user_id" = $2 LIMIT $3 [["user_id", "274d2f54-2418-4dec-a696-a5f62196ee85"], ["user_id", "44a0c35a-b5c1-4bb6-a8a3-30ae3d4b3345"], ["LIMIT", 11]]
=> #<ActiveRecord::Relation []>
class Conversation < ApplicationRecord
has_many :user_conversations
has_many :users, through: :user_conversations
def self.between(*users)
users.map do |u|
where("EXISTS(SELECT * FROM user_conversations uc WHERE uc.conversation_id = conversations.id AND uc.user_id = ?)", u)
end.reduce(&:merge)
.joins(:user_conversations)
.group(:id)
.having('count(*) = ?', users.length)
end
end
Not the cleanest solution ever but what is does is check for the existance of a join record for each user and then .having('count(*) = ?', users.length) ensures that there are not more join records then users.
Example usage:
Conversation.between(User.first, User.second)
Conversation.between(1,2,3)
Conversation.between(*User.all) # splat it like a pro
This results in the following SQL query:
SELECT "conversations".* FROM "conversations"
INNER JOIN "user_conversations" ON "user_conversations"."conversation_id" = "conversations"."id"
WHERE
(EXISTS(SELECT * FROM user_conversations uc WHERE uc.conversation_id = conversations.id AND uc.user_id = 1))
AND
(EXISTS(SELECT * FROM user_conversations uc WHERE uc.conversation_id = conversations.id AND uc.user_id = 2))
GROUP BY "conversations"."id"
HAVING (count(*) = 2)
LIMIT $1
You can do a query with AND condition as follows
Conversation.joins(:conversation_users).where(conversation_users: {user_id: User.first.id}).where(conversation_users: {user_id: User.second.id})
Using Rails 6 and CanCanCan. Here are my models:
class Shop < ApplicationRecord
has_many :shop_reviews, dependent: :destroy
end
class ShopReview < ApplicationRecord
belongs_to :shop, counter_cache: true
belongs_to :user_profile, counter_cache: true
end
class User < ApplicationRecord
has_one :user_profile, dependent: :destroy
has_many :shop_reviews, through: :user_profile
end
class UserProfile < ApplicationRecord
belongs_to :user
has_many :shop_reviews
end
ShopReview table:
id, shop_id, user_profile_id, review
Association:
1. A shop can have many reviews
2. Only one User Profile can have one review on a shop
Abilities I want to define:
1. User Profile can edit, update, destroy his own review of a shop
2. User Profile cannot create a new review if a review of his exist on that shop
What I tried:
class Ability
include CanCan::Ability
def initialize(user)
can :read, :all
if user.present?
can [:update, :destroy], ShopReview, user_profile_id: user.user_profile.id
can :create, ShopReview do
!ShopReview.exists?(user_profile_id: user.user_profile.id, shop_id: :shop_id)
end
end
end
end
But I can't seem to pass in the shop_id on visiting /shops/11/shop_reviews/new. Here's the log:
Started GET "/shops/11/shop_reviews/new" for 127.0.0.1 at 2020-01-04 00:23:10 +0800
Processing by ShopReviewsController#new as HTML
Parameters: {"shop_id"=>"11"}
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["id", 9], ["LIMIT", 1]]
UserProfile Load (0.2ms) SELECT "user_profiles".* FROM "user_profiles" WHERE "user_profiles"."user_id" = $1 LIMIT $2 [["user_id", 9], ["LIMIT", 1]]
↳ app/models/ability.rb:9:in `initialize'
ShopReview Exists? (0.3ms) SELECT 1 AS one FROM "shop_reviews" WHERE "shop_reviews"."user_profile_id" = $1 AND "shop_reviews"."shop_id" = $2 LIMIT $3 [["user_profile_id", 8], ["shop_id", nil], ["LIMIT", 1]]
↳ app/models/ability.rb:11:in `block in initialize'
Shop Load (0.2ms) SELECT "shops".* FROM "shops" WHERE "shops"."id" = $1 ORDER BY "shops"."created_at" DESC LIMIT $2 [["id", 11], ["LIMIT", 1]]
You should use gem pundit to create authorizations in your app. Pundit is easy to scale than gem cancancan. Because pundit allows your app to create authorization on each model. You can search more about two gems and choose what is most suitable for you.
I have a Rails (5.2.2) app (Postgres database) with some Models related to different geographies:
- districts (have many sectors)
-- sectors (have many cells, have one district)
--- cells (have many villages, have one sector)
---- villages (have many facilities, have one cell)
----- facilities (have one village)
I also have a Report Model, which, for context, records the quantity of specific technology distributed in a specific location.
#<Report id: nil, date: nil, technology_id: nil, user_id: nil, contract_id: nil, model_gid: nil, distributed: nil, checked: nil, created_at: nil, updated_at: nil, people: nil, households: nil>
This location can be any of the geography models. So I'm using GlobalID stored as a string in model_gid on the Report record.
e.g.:
#<Report id: 1, ... model_gid: "gid://liters-tracker/Village/64", ...>
Then I wrote some scopes that work fine:
scope :only_districts, -> { where('model_gid ILIKE ?', '%/District/%') }
scope :only_sectors, -> { where('model_gid ILIKE ?', '%/Sector/%') }
scope :only_cells, -> { where('model_gid ILIKE ?', '%/Cell/%') }
scope :only_villages, -> { where('model_gid ILIKE ?', '%/Village/%') }
scope :only_facilities, -> { where('model_gid ILIKE ?', '%/Facility/%') }
I thought this was a good approach because my report.model method works:
def model
GlobalID::Locator.locate model_gid
end
e.g.:
2.4.5 :001 > Report.first.model
Report Load (0.5ms) SELECT "reports".* FROM "reports" ORDER BY "reports"."id" ASC LIMIT $1 [["LIMIT", 1]]
Village Load (0.4ms) SELECT "villages".* FROM "villages" WHERE "villages"."id" = $1 LIMIT $2 [["id", 64], ["LIMIT", 1]]
=> #<Village id: 64, name: "Ruhanga", cell_id: 11, gis_id: 13080406, latitude: -2.00828333333333, longitude: 30.1708, population: 518, households: 179, created_at: "2019-01-21 22:53:06", updated_at: "2019-01-21 22:53:06">
I opted to do this string field instead of a polymorphic association because GlobalID::Locator methods can accept strings and parse out the model and ID from it. So why hassle with the association? Maybe this is the fundamental flaw in my thinking?
Because finding records based upon the model_gid seems to fail:
2.4.5 :045 > Report.all.where(model_gid: Report.first.model_gid)
Report Load (0.4ms) SELECT "reports".* FROM "reports" ORDER BY "reports"."id" ASC LIMIT $1 [["LIMIT", 1]]
Report Load (0.5ms) SELECT "reports".* FROM "reports" WHERE "reports"."model_gid" = $1 LIMIT $2 [["model_gid", "--- gid://liters-tracker/Village/64\n"], ["LIMIT", 11]]
=> #<ActiveRecord::Relation []>
(honestly not sure why the SQL is morphed into "--- gid://liters-tracker/Village/64\n" and if this is actually my problem)
2.4.5 :046 > Report.all.where("model_gid ILIKE ?", Report.first.model_gid)
Report Load (0.5ms) SELECT "reports".* FROM "reports" ORDER BY "reports"."id" ASC LIMIT $1 [["LIMIT", 1]]
Report Load (3.2ms) SELECT "reports".* FROM "reports" WHERE (model_gid ILIKE 'gid://liters-tracker/Village/64') LIMIT $1 [["LIMIT", 11]]
=> #<ActiveRecord::Relation []>
2.4.5 :049 > Report.all.where("model_gid = ?", Report.first.model_gid)
Report Load (0.3ms) SELECT "reports".* FROM "reports" ORDER BY "reports"."id" ASC LIMIT $1 [["LIMIT", 1]]
Report Load (0.6ms) SELECT "reports".* FROM "reports" WHERE (model_gid = 'gid://liters-tracker/Village/64') LIMIT $1 [["LIMIT", 11]]
=> #<ActiveRecord::Relation []>
I'm trying to get this method to work:
def self.related_to(record)
where(model_gid: record.to_global_id.to_s)
end
And I really don't understand why it's not working:
2.4.5 :010 > Report.first.model_gid
Report Load (0.6ms) SELECT "reports".* FROM "reports" ORDER BY "reports"."id" ASC LIMIT $1 [["LIMIT", 1]]
=> "gid://liters-tracker/Village/64"
2.4.5 :011 > Village.find(64).to_global_id.to_s
Village Load (0.5ms) SELECT "villages".* FROM "villages" WHERE "villages"."id" = $1 LIMIT $2 [["id", 64], ["LIMIT", 1]]
=> "gid://liters-tracker/Village/64"
2.4.5 :012 > Report.first.model_gid == Village.find(64).to_global_id.to_s
Report Load (0.4ms) SELECT "reports".* FROM "reports" ORDER BY "reports"."id" ASC LIMIT $1 [["LIMIT", 1]]
Village Load (0.3ms) SELECT "villages".* FROM "villages" WHERE "villages"."id" = $1 LIMIT $2 [["id", 64], ["LIMIT", 1]]
=> true
2.4.5 :013 > Report.all.where(model_gid: Village.find(64).to_global_id.to_s)
Village Load (0.4ms) SELECT "villages".* FROM "villages" WHERE "villages"."id" = $1 LIMIT $2 [["id", 64], ["LIMIT", 1]]
Report Load (0.4ms) SELECT "reports".* FROM "reports" WHERE "reports"."model_gid" = $1 LIMIT $2 [["model_gid", "--- gid://liters-tracker/Village/64\n"], ["LIMIT", 11]]
=> #<ActiveRecord::Relation []>
If I mimic the scopes, it does work:
def self.related_to(record)
where('model_gid ILIKE ?', "%#{record.to_global_id.to_s}%")
end
But, in the example records I've been showing, this would match Village #64 and Village #640, so it's not a good solution.
UPDATE
I thought maybe the special characters were throwing things off. But things work as expected when I use another string column on another Model:
2.4.5 :052 > Village.first.update(name: "gid://liters-tracker/Village/64")
Village Load (0.5ms) SELECT "villages".* FROM "villages" ORDER BY "villages"."id" ASC LIMIT $1 [["LIMIT", 1]]
(0.2ms) BEGIN
Cell Load (0.2ms) SELECT "cells".* FROM "cells" WHERE "cells"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
Village Exists (0.3ms) SELECT 1 AS one FROM "villages" WHERE "villages"."gis_id" = $1 AND "villages"."id" != $2 LIMIT $3 [["gis_id", 11070101], ["id", 1], ["LIMIT", 1]]
Village Update (0.3ms) UPDATE "villages" SET "name" = $1, "updated_at" = $2 WHERE "villages"."id" = $3 [["name", "gid://liters-tracker/Village/64"], ["updated_at", "2019-07-06 22:16:38.585563"], ["id", 1]]
(1.2ms) COMMIT
=> true
2.4.5 :053 > Village.where(name: "gid://liters-tracker/Village/64")
Village Load (0.3ms) SELECT "villages".* FROM "villages" WHERE "villages"."name" = $1 LIMIT $2 [["name", "gid://liters-tracker/Village/64"], ["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Village id: 1, name: "gid://liters-tracker/Village/64", cell_id: 1, gis_id: 11070101, latitude: -2.054922, longitude: 30.0912883, population: 513, households: 110, created_at: "2019-01-21 22:53:04", updated_at: "2019-07-06 22:16:38">]>
I thought maybe I needed an index on the Report.model_gid field. But it hasn't made a difference.
class AddModelGidIndexToReports < ActiveRecord::Migration[5.2]
def change
add_index :reports, :model_gid
end
end
UPDATE 2
(this is based upon my own provided 'answer', but since it's a question, I put it here)
#MichaelChaney:
Just so I'm clear, are you suggesting something like this:
class Report < ApplicationRecord
belongs_to :technology, inverse_of: :reports
belongs_to :user, inverse_of: :reports
belongs_to :contract, inverse_of: :reports
enum geography: { district: 'district', sector: 'sector', cell: 'cell', village: 'village', facility: 'facility' }
At this point, should I just add a geography_id integer column and stop using GlobalID?
What about just going polymorphic instead?
For the sake of closing the record, I switched to a polymorphic association. Probably not quite as fast as the Enum solution that #MichaelChaney suggests in the comments on the previous answer, but fast enough for my in-house app and creates an association known to my app.
class Report < ApplicationRecord
belongs_to :technology, inverse_of: :reports
belongs_to :user, inverse_of: :reports
belongs_to :contract, inverse_of: :reports
# serialize :model_gid #<-- this was real bad as #MichaelChaney points out
# enum geography: { district: 'district', sector: 'sector', cell: 'cell', village: 'village', facility: 'facility' } #<-- this is probably the fastest option
belongs_to :reportable, polymorphic: true #<-- this is probably the middle ground, as the :reportable_id and :reportable_type columns are indexed together
And this was partnered with the following on all my Geography models, e.g.:
class Facility < ApplicationRecord
belongs_to :village, inverse_of: :facilities
has_one :cell, through: :village, inverse_of: :facilities
has_one :sector, through: :cell, inverse_of: :facilities
has_one :district, through: :sector, inverse_of: :facilities
has_many :reports, as: :reportable, inverse_of: :reportable #<-- tadaa
So now I don't even need my initial method as I can compare the results of reports.reportable with the record I have to see if they are associated.
The lesson I learned: in the early stages, I need to think more about RdBMS and what associations I'll care a lot about so I don't try doing dumb Regex searches across my dB.
The other lesson: keep better notes in my code base, so when I change strategies I can correctly un-wind things I implemented.
Ugh. This is a face-palm moment.
class Report < ApplicationRecord
belongs_to :technology, inverse_of: :reports
belongs_to :user, inverse_of: :reports
belongs_to :contract, inverse_of: :reports
serialize :model_gid
scope :only_districts, -> { where('model_gid ILIKE ?', '%/District/%') }
...
Report.model_gid is serialized, which I did before I discovered GlobalID. I think I was planning to save some key-value hash like {model: 'Village', id: '64'}.
Now to figure out how to un-serialize a column.
I am not very good at learning how to code. I've been trying for 4 years and still struggling to figure out basic concepts.
I haven't found the right starting point so, I'm constantly gap filling for things that might be foundations and I don't know them yet.
I have a Rails 5 app. It has models for user, roles, app_roles and assign_roles.
The associations are:
User
rolify strict: true # strict means you get true only on a role that you manually add
attr_accessor :current_role
belongs_to :organisation
Role
has_and_belongs_to_many :users, :join_table => :users_roles
belongs_to :resource,
:polymorphic => true,
:optional => true
AppRole
[no associations]
Note: this is the resource I'm using to have a CRUD to define roles that the app can use to assign roles to users.
Assign Role
[no associations]
Note: this is the resource I'm using to allow some users to assign roles to some other users.
Organisation
has_many :users
I'm using rolify gem.
My user model has:
class User < ApplicationRecord
rolify strict: true # strict means you get true only on a role that you manually add
attr_accessor :current_role
My Roles table has:
class Role < ApplicationRecord
has_and_belongs_to_many :users, :join_table => :users_roles
belongs_to :resource,
:polymorphic => true,
:optional => true
validates :resource_type,
:inclusion => { :in => Rolify.resource_types },
:allow_nil => true
scopify
end
In my assign_roles controller, I have:
class Users::AssignRolesController < ApplicationController
before_action :authenticate_user!
def index
# if #current_user.is_admin?
#app_roles = AppRole.all
# else
# #app_roles = AppRole.where(category: relevant_category)
# end
# if #current_user.is_admin?
#users = User.all
# else
# #users = current_user.organisation.users.select { |u| u.id != current_user.organisation.owner_id }
# end
end
def create
user = User.find(params[:users])
role = AppRole.find(params[:roles])
# organisation = Organisation.first
# #organisation = Organisation.find(#current_user.organisation)
# byebug
user.add_role role.display_name, #current_user.organisation
flash[:notice] = "Successfully created"
redirect_to action: :index
end
def show
# #users = User.joins(:profiles).where('profiles.organisation_id = ?' #current_user.organisation.id)
# #users = User.all
#current_user.organisation.users
end
def update
end
def destroy
user = User.find(params[:users])
# role = AppRole.find(params[:roles])
assigned_role = user.roles
# user_roles = user.roles
# organisation = Organisation.first
# byebug
user.remove_role assigned_role.name, #current_user.organisation
flash[:notice] = "Successfully created"
redirect_to action: :index
end
end
In my routes file, i have:
resources :users, shallow: true do
scope module: :users do
resources :assign_roles
resources :identities
end
end
In my views/users/assign_roles/index file, I have:
<%= form_tag(url: '/assign_roles', method: :post ) do |f| %>
<div class="row" style="margin-top:50px; margin-bottom: 150px">
<div class="col-md-3 col-md-offset-1">
<%= select_tag "users", options_from_collection_for_select(#users, "id", "full_name"), { class: "chosen-select form-control" } %>
</div>
<!-- # roles -->
<div class="col-md-3 col-md-offset-1">
<%= select_tag "roles", options_from_collection_for_select(#app_roles, "id", "display_name"), :class => 'chosen-select form-control' %>
</div>
<div class="col-md-3 col-md-offset-1">
<div class="form-actions">
<%= submit_tag(value = "Submit") %>
</div>
</div>
</div>
<% end %>
So far, that all looks OK. Im' getting stuck at the next part.
In my views/users/assign_roles/show.html.er
I'm trying to show each user's roles on a view. I want the user that assigned any existing roles to be able to delete them.
I have:
<% #users.each do |user| %>
<td><%= user.roles.count %></td>
<% user.roles.each do |role| %>
<td><%= role.name.titleize %></td>
<td><%= link_to 'Destroy', role, method: :delete, data: { confirm: 'Are you sure?' } %></td>
<% end %>
<% end %>
My controller for assign_roles is saved in app/controllers/users folder. My views folder for assign roles is saved inside app/views/users/assign_roles
When I try to use the app/users/4/assign_roles form, I get the form to render with the list of AppRoles that are available to be assigned. I don't get any error message. Instead, I get the notice on successful creation. However, when I try to check if a user has a role, I get false.
I can see the server log reads as follows:
Started POST "/users/4/assign_roles?method=post&url=%2Fassign_roles" for ::1 at 2016-10-23 11:50:11 +1100
Processing by Users::AssignRolesController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"U80gMQd01SaTcHHnnSFxvRc2u9JvJMFB+5smS9SaN8ZRixQvJRTMbutG0KkoqXL+oMU1aOxX8AURBtuy2Rm5yA==", "users"=>"4", "roles"=>"3", "commit"=>"Submit", "method"=>"post", "url"=>"/assign_roles", "user_id"=>"4"}
User Load (0.8ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["id", 4], ["LIMIT", 1]]
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 4], ["LIMIT", 1]]
AppRole Load (0.2ms) SELECT "app_roles".* FROM "app_roles" WHERE "app_roles"."id" = $1 LIMIT $2 [["id", 3], ["LIMIT", 1]]
Organisation Load (0.4ms) SELECT "organisations".* FROM "organisations" ORDER BY "organisations"."id" ASC LIMIT $1 [["LIMIT", 1]]
Role Load (1.1ms) SELECT "roles".* FROM "roles" WHERE "roles"."name" = $1 AND "roles"."resource_type" = $2 AND "roles"."resource_id" = $3 ORDER BY "roles"."id" ASC LIMIT $4 [["name", "sdfddd"], ["resource_type", "Organisation"], ["resource_id", 1], ["LIMIT", 1]]
(0.2ms) BEGIN
(0.1ms) ROLLBACK
HABTM_Roles Load (0.4ms) SELECT "users_roles".* FROM "users_roles" WHERE "users_roles"."user_id" = $1 [["user_id", 4]]
Role Load (0.3ms) SELECT "roles".* FROM "roles" WHERE "roles"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
(0.5ms) SELECT "roles".id FROM "roles" INNER JOIN "users_roles" ON "roles"."id" = "users_roles"."role_id" WHERE "users_roles"."user_id" = $1 [["user_id", 4]]
Role Load (0.3ms) SELECT "roles".* FROM "roles" WHERE "roles"."id" = 1
Role Load (0.4ms) SELECT "roles".* FROM "roles" INNER JOIN "users_roles" ON "roles"."id" = "users_roles"."role_id" WHERE "users_roles"."user_id" = $1 [["user_id", 4]]
Redirected to http://localhost:3000/users/4/assign_roles
Completed 302 Found in 33ms (ActiveRecord: 6.5ms)
I don't know what the ROLLBACK line means but I don't think that usually appears in these log messages when things are working correctly.
Can anyone see what I need to do to get the role assignment working correctly?
organisation model
class Organisation < ApplicationRecord
include LogoUploader[:logo]
include BannerUploader[:banner]
# --------------- associations
has_many :users
# --------------- scopes
# --------------- validations
# --------------- class methods
enum org_type: {
University: 1,
Publicly_Funded_Research_Organisation: 2,
Industry: 3,
Grantor: 4,
Investor: 5,
Policy: 6,
}
# --------------- callbacks
# --------------- instance methods
# --------------- private methods
end
add resourcify to your organisation model and then try.
Instead of developing our own RBAC (Role Based Access Control), there are already some open source gems available in rails community. we can make use of that.
Its a nice gem
https://github.com/nathanl/authority
Also there are many other gems available
https://www.ruby-toolbox.com/categories/rails_authorization
If none of the above gems fulfil your need, then atleast you can use that as an sample model for your development.
In my controllers/common/roles_controller.rb I would like to check if particular role (ID) belongs to current_user company users and if not, redirect to errors_path:
def correct_role
role_user = Role.where(:id => params[:id]).select('user_id').first
company_user = current_user.companies.includes(:users)
redirect_to(errors_path) unless company_user.include? role_user.id
end
Definitions:
role_user - finds user ID for particular role ID ("user_id" is column of roles table)
company_user - finds all user ID who belong to companies, which belong to current_user
models/role.rb
belongs_to :user, optional: true, inverse_of: :roles
accepts_nested_attributes_for :user
enum general: { seller: 1, buyer: 2, seller_buyer: 3}, _suffix: true
enum dashboard: { denied: 0, viewer: 1, editer: 2, creater: 3, deleter: 4}, _suffix: true
models/user.rb
#User has roles
has_many :roles
accepts_nested_attributes_for :roles, reject_if: proc { |attributes| attributes[:name].blank? }
# User has many companies
has_many :accounts, dependent: :destroy
has_many :companies, through: :accounts
models/account.rb
class Account < ApplicationRecord
belongs_to :company
belongs_to :user
accepts_nested_attributes_for :company, :user
end
models/company.rb
has_many :accounts, dependent: :destroy
has_many :users, through: :accounts
At the moment with role_user and company_user I can find both ID, however I cannot do the checking part. How do I do that correctly, please? Thank you for any help!
Update
#sajan code give this in console when I open /common/roles/1/edit (current_user ID=1 and should be allowed to edit):
Parameters: {"id"=>"1"}
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
(0.6ms) SELECT COUNT(*) FROM "companies" INNER JOIN "accounts" ON "companies"."id" = "accounts"."company_id" WHERE "accounts"."user_id" = ? [["user_id", 1]]
(0.3ms) SELECT "roles".id FROM "roles" WHERE "roles"."user_id" = ? [["user_id", 1]]
#Abhishek Kumar code in console gives:
Parameters: {"id"=>"1"}
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
Company Load (0.5ms) SELECT "companies".* FROM "companies" INNER JOIN "accounts" ON "companies"."id" = "accounts"."company_id" WHERE "accounts"."user_id" = ? [["user_id", 1]]
(0.4ms) SELECT "users".id FROM "users" INNER JOIN "accounts" ON "users"."id" = "accounts"."user_id" WHERE "accounts"."company_id" = ? [["company_id", 13]]
Role Load (0.2ms) SELECT "roles"."user_id" FROM "roles" WHERE "roles"."id" = ? ORDER BY "roles"."id" ASC LIMIT ? [["id", 1], ["LIMIT", 1]]
Update v2
So I'm trying to use this code:
def correct_role
company_user_ids = current_user.companies.map(&:user_ids)
role_user = Role.where(:id => params[:id]).select('user_id').first
unless role_user.user_id.in?(company_user_ids)
redirect_to(errors_path)
end
end
however it redirects to errors_path in any case, this is what I have in console:
Parameters: {"id"=>"1"}
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
Company Load (0.5ms) SELECT "companies".* FROM "companies" INNER JOIN "accounts" ON "companies"."id" = "accounts"."company_id" WHERE "accounts"."user_id" = ? [["user_id", 1]]
(0.4ms) SELECT "users".id FROM "users" INNER JOIN "accounts" ON "users"."id" = "accounts"."user_id" WHERE "accounts"."company_id" = ? [["company_id", 13]]
Role Load (0.3ms) SELECT "roles"."user_id" FROM "roles" WHERE "roles"."id" = ? ORDER BY "roles"."id" ASC LIMIT ? [["id", 1], ["LIMIT", 1]]
It seems that a better logic would be to define on the model for the current_user an association through companies and users to roles.
Then you can check:
def correct_role
redirect_to(errors_path) unless current_user.company_user_roles.where(id: params[:id]).exists?
end
It would be a single SQL statement that would return zero or one rows, and execute very quickly with the appropriate indexes in place.
Edit:
To company.rb, add:
has_many :user_roles, through: :users, source: :roles
To user.rb (assuming that current_user is an instance of this model) add:
has_many :company_user_roles, through: :companies, source: :user_roles
Maybe you could do like this:
def correct_role
unless current_user.companies.count > 0 && current_user.role_ids.include?(params[:id])
redirect_to(errors_path)
end
end