Cancan Accessible By Not Displaying All Records - ruby-on-rails

Is there no-one out there who can assist with this?
Having problems finding records using cancan in my rails 3.2 app.
In my 'nas' index, I'm trying to display a list of 'nas' that belong to the current_user's locations.
The issue seems to be that there's no formal relationship between the user and the nas - the location owns the nas and the user owns the location.
Using the accessible_by method in my nas controller gives me unusual results. If I use the following in my ability.rb, I get an error :
can :read, Nas, :locationusers => { :user_id => user.id }
Error: undefined method `class_name' for nil:NilClass
And, if I change to:
can :read, Nas, :locations => { :user_id => user.id }
I only get the nas listed for the users first location.
For example, if my user has locations with ids = 1,2,3 only the nas from location 1 are displayed.
Is there a way to display all the nas for the current user's locations using cancan or do I have to go about this differently?
My relationships are as follows:
class User < ActiveRecord::Base
...
has_many :locationusers
has_many :locations, :through => :locationusers
...
end
class Location < ActiveRecord::Base
...
has_many :locationusers
has_many :users, :through => :locationusers
has_many :nas
...
end
class Node < ActiveRecord::Base
...
belongs_to :location
end
In my ability.rb:
...
if user.role? :customer_admins
can :read, Nas, :locations => { :user_id => user.id }
..
NasController
#nas = Nas.accessible_by(current_ability).all

With Cancan, the User is king. If you cannot draw a line from User to the Model you are trying to authorize, it is not going to work. The easy solution is to model the relationship between Nas (the model in your example is called Node?) and User. This can be done via Locationuser (which is not shown in your example) as a has_many :through using the existing belongs_to.
class Node < ActiveRecord::Base
...
belongs_to :location
has_many :locationusers, :through => :location
end
Now in your Cancan ability.rb, you can use:
can :read, Node, :locationusers => { :user_id => user.id }
(I changed Nas from your example to Node to correctly match the model)

Related

How to detect changes in has_many through association?

I have the following models.
class Company < ApplicationRecord
has_many :company_users
has_many :users, :through => :company_users
after_update :do_something
private
def do_something
# check if users of the company have been updated here
end
end
class User < ApplicationRecord
has_many :company_users
has_many :companies, :through => :company_users
end
class CompanyUser < ApplicationRecord
belongs_to :company
belongs_to :user
end
Then I have these for the seeds:
Company.create :name => 'Company 1'
User.create [{:name => 'User1'}, {:name => 'User2'}, {:name => 'User3'}, {:name => 'User4'}]
Let's say I want to update Company 1 users, I will do the following:
Company.first.update :users => [User.first, User.second]
This will run as expected and will create 2 new records on CompanyUser model.
But what if I want to update again? Like running the following:
Company.first.update :users => [User.third, User.fourth]
This will destroy the first 2 records and will create another 2 records on CompanyUser model.
The thing is I have technically "updated" the Company model so how can I detect these changes using after_update method on Company model?
However, updating an attribute works just fine:
Company.first.update :name => 'New Company Name'
How can I make it work on associations too?
So far I have tried the following but no avail:
https://coderwall.com/p/xvpafa/rails-check-if-has_many-changed
Rails: if has_many relationship changed
Detecting changes in a rails has_many :through relationship
How to determine if association changed in ActiveRecord?
Rails 3 has_many changed?
There is a collection callbacks before_add, after_add on has_many relation.
class Project
has_many :developers, after_add: :evaluate_velocity
def evaluate_velocity(developer)
#non persisted developer
...
end
end
For more details: https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Association+callbacks
You can use attr_accessor for this and check if it changed.
class Company < ApplicationRecord
attr_accessor :user_ids_attribute
has_many :company_users
has_many :users, through: :company_users
after_initialize :assign_attribute
after_update :check_users
private
def assign_attribute
self.user_ids_attribute = user_ids
end
def check_users
old_value = user_ids_attribute
assign_attribute
puts 'Association was changed' unless old_value == user_ids_attribute
end
end
Now after association changed you will see message in console.
You can change puts to any other method.
I have the feelings you are asking the wrong question, because you can't update your association without destroy current associations. As you said:
This will destroy the first 2 records and will create another 2 records on CompanyUser model.
Knowing that I will advice you to try the following code:
Company.first.users << User.third
In this way you will not override current associations.
If you want to add multiple records once try wrap them by [ ] Or ( ) not really sure which one to use.
You could find documentation here : https://guides.rubyonrails.org/association_basics.html#has-many-association-reference
Hope it will be helpful.
Edit:
Ok I thought it wasn't your real issue.
Maybe 2 solutions:
#1 Observer:
what I do it's an observer on your join table that have the responsability to "ping" your Company model each time a CompanyUser is changed.
gem rails-observers
Inside this observer call a service or whatever you like that will do what you want to do with the values
class CompanyUserObserver < ActiveRecord::Observer
def after_save(company_user)
user = company_user.user
company = company_user.company
...do what you want
end
def before_destroy(company_user)
...do what you want
end
end
You can user multiple callback in according your needs.
#2 Keep records:
It turn out what you need it keep records. Maybe you should considerate use a gem like PaperTrail or Audited to keep track of your changes.
Sorry for the confusion.

How to structure a has_many association with a dynamic scope?

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.

Is it possible to assign multiple has_many ":as" polymorphic relation on same field?

Having my db setup like this ("type" is always User although I have different kind of User via STI):
class User
# fields
# :id
# :sender_id, :sender_type
# :recipient_id, :recipient_type
end
Postcard model:
class Postcard < ActiveRecord::Base
belongs_to :owner, :class_name => User
belongs_to :recipient, :class_name => User
end
I'd like to setup the User model something like this:
class User < ActiveRecord::Base
has_many :postcards, :as => [:sender or :recipient] # this is not working
end
So I could say:
user.postcards
Is it possible?
PS: I've also tried this road:
has_many :postcards, :finder_sql => Proc.new { "SELECT * FROM postcards WHERE postcards.owner_id=#{id} OR postcards.recipient_id=#{id}" }
But found myself stuck on scopes as :finder_sql recreates a whole new SQL:
User.postcards.by_status('new').size
As mentioned by joelparkerhenderson I need to think differently my association strategy.
As I'd like to have:
user.postcards
My answer is simply to use scopes in Postcard model:
scope :of_user, lambda { |user| where("recipient_id = ? OR owner_id = ?", user.id, user.id) }
So I can invoke:
Postcard.of_user user
I could even wrap it in User model:
def postcards
Postcard.of_user self
end

How do I access records from a 2 level nested model

I have 3 models User,Listing and Message. What I want is for an authenticated user to have many listings. The listings then can have multiple messages. So the messages are tied to the user through the listing model. I am able to get a users listings but not able to get the users messages which he owns through the listings. Here are the associations that I currently have.
class User < ActiveRecord::Base
has_many :listings, :dependent => :destroy
end
class Listing < ActiveRecord::Base
belongs_to :user
has_many :messages
end
class Message < ActiveRecord::Base
belongs_to :listing
end
To create a message I simply do this;
#listing = Listing.find(params[:listing_id])
#message = #listing.messages.build(params[:message])
And getting the user's listing i have this;
#user_listings = Listing.user_listings(current_user)
But getting the messages tied to the user's listings proves to be elusive. What am I doing wrong or how do I go about this? help appreciated.
Still not sure where user_listings comes from but why not this:
#user = User.find(params[:user_id], :include => {:listings => :messages})
#user.listings.each do |listing|
listing.messages.each do |message|
#or
#user.listings.collect(&:messages).each do |message|
#or (just read about using authenticated user so the same as above like this
current_user.listings(:all, :include => :messages)...
Include prefetches all the listings' associated messages in one query in order that they're not fetched in the loop causing n+1 querying.
----------
Or another approach, if you don't need the listings data.
#messages.rb
def self.user_messages user_id
find(:all, :joins => :listings, :conditions => ["listings.user_id = ?", user_id])
#with pagination
def self.user_messages user_id, page
paginate(:all, :joins => :listings,
:conditions => ["listings.user_id = ?", user_id],
:per_page => 10, :page => page)
updated regarding your comment.
You may want to just add has_many :messages to the user class as well and add a user_id column to Message. Then you could just do current_user.messages
How about something like this:
class User < ActiveRecord::Base
has_many :listings, :dependent => :destroy
has_many :listing_messages, :through => :listings
That way you dont have to "tie" the messages with the user because it is always accessed through the listing association:
current_user.listing_messages.all
Or have I misunderstood your question?
If you have current_user pulled already. You can just access listings directly by calling
current_user.listings
instead of
#user_listings = Listing.user_listings(current_user)

Searching polymorphic associations in Rails

I have a need in my app to allow users to bookmark a post. They should only be able to create one bookmark per post. I've set up my polymorphic association like so:
class Post < ActiveRecord::Base
has_many :bookmarks, :as => :bookmarkable
end
class Bookmark < ActiveRecord::Base
belongs_to :bookmarkable, :polymorphic => true
belongs_to :user
end
class User < ActiveRecord:Base
has_many :posts
has_many :bookmarks
end
In my view, a user can create a bookmark. I would like to find some way to replace the "Create Bookmark" view code with "Delete Bookmark" code, if the user has already bookmarked a particular post.
If I try to do something like this:
#post = Post.find(params[:id, :include => [:bookmarks]])
- if #post.bookmarks.users.include?(#user)
I get a No Method error for "users"
How can I access owners of the bookmarks, to determine if the current user has already bookmarked a page?
Thank you.
I would approach this from the user's point of view:
class User < ActiveRecord::Base
has_many :posts
has_many :bookmarks
# Rails 3
def bookmarked?(post)
bookmarks.where(
{
:bookmarkable_id => post.id, :bookmarkable_type => post.class.name
}
).count > 0
end
# Rails 2
def bookmarked?(post)
bookmarks.find(:all, :conditions =>
{
:bookmarkable_id => post.id, :bookmarkable_type => post.class.name
}
).count > 0
end
end
if #user.bookmarked?(#post)
# Show delete link
else
# Show bookmark link
end
I would also advice you to add a validation to your bookmarks model that prevents a user from bookmarking the same post twice.

Resources