I have a model:
class TenantReference < ActiveRecord::Base
include TenantReferenceAdmin
belongs_to :tenant, inverse_of: :reference
default_scope { eager_load(:tenant) }
end
and a Tenant model:
class Tenant < ActiveRecord::Base
default_scope { eager_load(:user) }
belongs_to :user
end
and a User model:
class User < ActiveRecord::Base
has_many :tenants, :foreign_key => :user_id, class_name: 'Tenant'
end
and finally a TenantReferenceAdmin rails admin file:
module TenantReferenceAdmin
extend ActiveSupport::Concern
included do
rails_admin do
list do
field :tenant do
filterable true
queryable true
searchable [ :first_name, :last_name]
end
...
what I'm trying to achieve is that in the tenantreference admin page the user can search TenantReference objects by the first_name or last_name of the user through their Tenant reference.
This configuration is producing a postgresql query like:
SELECT "tenant_references"."id" AS t0_r0, .... "tenants"."id" AS t1_r0, ......
FROM "tenant_references" LEFT OUTER JOIN "tenants" ON "tenants"."id" = "tenant_references"."tenant_id"
WHERE ((tenants.first_name ILIKE '%query%')
OR (tenants.last_name ILIKE '%query%')
ORDER BY tenant_references.id desc LIMIT 20 OFFSET 0)
which doesn't work, because first/last_name are actually fields of user, not of tenant.
how could I fix this?
Thanks,
The problem seems to be that rails admin only adds JOIN to the query if the current model has a direct link (has_many, has_one ...) to the other model to search in. and it joins it if the corresponding field is marked as queryable true.
so I changed adding to the references model this line:
has_one :user, through: :tenant
I then created an invisible list field:
field :user do
visible false
queryable true
searchable [{User => :first_name}, {User => :last_name}]
end
that can be searched upon, but it's not shown in the list.
this has solved the issue, I don't consider this ideal as I had to modify my model in order to be able to perform rails_admin search, when rails_admin could have handled this situation without changing the code. But for now I can live with this
Related
I have a polymorphic association and sometimes I want to preload it's associations.
When I left join the model, my WHERE filters get lost because they don't referenced the named association.
SELECT COUNT(*) FROM `companies` LEFT OUTER JOIN `key_values` `latest_information` ON `latest_information`.`attachment_id` = `companies`.`id` AND `latest_information`.`attachment_type` = 'Company' AND `key_values`.`name` = 'latest_information' WHERE `latest_information`.`id` IS NOT NULL
# => ActiveRecord::StatementInvalid: Mysql2::Error: symbol key_values.`name` not found
This is the query that is generated but it's invalid due to the key_values.name not being referenced.
Here's what my model looks like:
class Company < LeadRecord
has_many :key_values, as: :attachment, dependent: :delete_all
has_one :latest_information,
-> { KeyValue.latest('latest_information') },
class_name: KeyValue.to_s,
as: :attachment
end
class KeyValue < LeadRecord
belongs_to :attachment, polymorphic: true
def self.latest(name)
order(created_at: :desc).where(name: name) # This is the source of the error
end
end
I can probably fix this by passing addition parameters to self.latest such as the association name but I want to know if there's a better Rails way to do this.
In the interim I have solved this by making this change on KeyValue.
# key_value.rb
def self.latest(name, association_name = 'key_values')
order(created_at: :desc).where("#{association_name}.name = ?", name)
end
# company.rb
has_one association_name,
-> {
KeyValue.latest(
method_name.to_s,
association_name.to_s,
)
},
class_name: KeyValue.to_s,
as: :attachment
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.
I am trying to build an active record query using through table associations. Here are my models:
Event.rb:
has_many :event_keywords
User.rb:
has_many :user_keywords
Keyword.rb:
has_many :event_keywords
has_many :user_keywords
EventKeyword.rb:
belongs_to :event
belongs_to :keyword
UserKeyword.rb:
belongs_to :user
belongs_to :keyword
I am trying to build an Event scope that takes a user_id as a param and returns all the Events with shared keywords. This was my attempt but it's not recognizing the user_keywords association:
scope :with_keywords_in_common, ->(user_id) {
joins(:event_keywords).joins(:user_keywords)
.where("user_keywords.user_id = ?", user_id)
.where("event_keywords.keyword_id = user_keywords.keyword_id")
}
How do I resolve this?
Something like this might work. 2-step process. First, get all user's keywords. Then find all events with the same keyword.
scope :with_keywords_in_common, ->(user) {
joins(:event_keywords).
where("event_keywords.keyword_id" => user.user_keywords.pluck(:id))
}
The database seems to be overkill here and firstly I'd simplify by making keywords polymorphic, this would get rid of 2 of your tables here (event_keywords, and user_keywords).
Your associations would then look like this:
# Event.rb:
has_many :keywords, as: keywordable
# User.rb:
has_many :keywords, as: keywordable
# Keyword.rb:
belongs_to :keywordable, polymorphic: true
And finally, your scope:
scope :with_keywords_in_common, -> (user_id) do
joins(:keywords)
.where('keywords.keywordable_type = User AND keywords.word IN (?)', keywords.pluck(:name))
end
My user model has a default scope to not show deleted users. This allows my app to soft delete users. The problem is this is causing an error in an associated model.
User.rb (id,name)
default_scope :conditions => 'users.deleted_at IS NULL'
NavItem.rb (id,user_id, friend_id)
items = NavItem.find_all_by_user_id(current_user.id)
items.each do |item|
user = User.find(item.friend_id)
end
The problem I'm having here is that when a user is set is deleted, meaning #user.deleted_at is NOT NULL, the query above for the user errors because no user is found.
How can I update NavItem.rb so it is joined to the User model and magically filters out users.deleted_at?
Thanks
The following might help you.
I scaffolded this:
class User < ActiveRecord::Base
attr_accessible :deleted_at, :name
default_scope :conditions => 'users.deleted_at IS NULL'
has_many :nav_items
end
class NavItem < ActiveRecord::Base
attr_accessible :friend_id, :user_id
belongs_to :user
scope :without_deleted_users, where(:user_id => User.scoped)
end
And NavItem.without_deleted_users:
NavItem.without_deleted_users
NavItem Load (0.2ms) SELECT "nav_items".* FROM "nav_items" WHERE "nav_items"."user_id" IN (SELECT "users"."id" FROM "users" WHERE (users.deleted_at IS NULL))
A Contact has a User assigned to them:
class Contact < ActiveRecord::Base
...
belongs_to :user
...
end
The user model has a field I want to exclude any time a user object or objects are returned from db. One of the ways to make it work is to add a default scope:
class User < ActiveRecord::Base
...
has_many :contacts
...
default_scope select((column_names - ['encrypted_password']).map { |column_name| "`#{table_name}`.`#{column_name}`"})
end
So in console if I do:
User.first
The select statement and result set do not include 'encrypted_password'.
However, if I do:
c = Contact.includes(:user).first
c.user
they do. The default scope on the User model does not get applied in this case and the 'encrypted_password' field is shown.
So my question is why? And also, is there a clean way to specify what fields should be returned on related object(s)?
You should just be able to use the :select option on the belongs_to relationship. Something like this:
class Contact < ActiveRecord::Base
...
belongs_to :user, :select => [:id, :first_name, :last_name, :email]
...
end