Search field in rails 5 - ruby-on-rails

I have an index page with users list where I want to implement a search field, and filtering records based on the text field of users
scope :filter_users ->(params[:search]) { where("name like %#{params[:search]}%)
where i also need to check the the associated object name. I.e user belongs to organization, I need to add condition at scope to check the organisation name.

Your question was missing this, but I'll suppose your model is something like:
class User
belongs_to :organization
end
class Organization
has_many :users
end
To setup a search on the relation, you'll first need to join to the organization table onto users and search on the result of the join, filtering both on the user name and the organization name. It would look something like:
class User
belongs_to :organization
# I took the liberty of fixing your simple search syntax
scope :simple_search, ->(query) { where('name like ?', "%#{query}%") }
# The complex search:
# 1. Inner joins users table to organizations table
# 2. Applies a where conditions to the result of the join (note: we need to
# specify the table name in the where because both models have a name field)
scope :complex_search, ->(query) { joins(:organization).where('users.name LIKE :q OR organizations.name LIKE :q', query:"%#{query}%")}
end
And used like this:
# returns only users whose name matches '%ben%'
User.simple_search('ben')
# returns users whose name matches '%ben%' and users belonging to
# companies whose name matches '%ben%'
User.complex_search('ben')
You can find an example of this (and interesting details about things I used in my answer, and lots of other things as well) in the official Active Record Query Interface guide, and for this particular matter, in the specifying conditions on the joined tables section.

Related

Rails ActiveRecord/Arel query aggregate column with select()

I've got two basic models with a join table. I've added a scope to compute a count through the relation and expose it as an attribute/psuedo-column. Everything works fine, but I'd now like to query a subset of columns and include the count column, but I don't know how to reference it.
tldr; How can I include an aggregate such as a count in my Arel query while also selecting a subset of columns?
Models are Employer and Employee, joined through Job. Here's the relevant code from Employer:
class Employer < ApplicationRecord
belongs_to :user
has_many :jobs
has_many :employees, through: :jobs
scope :include_counts, -> do
left_outer_joins(:employees).
group("employers.id").
select("employers.*, count(employees.*) as employees_count")
end
end
This allows me to load an employer with counts:
employers = Employer.include_counts.where(id: 1)
And then reference the count:
count = employers[0].employees_count
I'm loading the record in my controller, which then renders it. I don't want to render more fields than I need to, though. Prior to adding the count, I could do this:
employers = Employer.where(id: 1).select(:id, :name)
When I add my include_counts scope, it basically ignores the select(). It doesn't fail, but it ends up including ALL the columns, because of this line in my scope:
select("employers.*, count(employees.*) as employees_count")
If I remove employers.* from the scope, then I don't get ANY columns in my result, with or without a select() clause.
I tried this:
employers = Employer.include_counts.where(id: 1).select(:id, :name, :employee_counts)
...but that produces the following SQL:
SELECT employers.*, count(employees.*) as employees_count, id, name, employees_count FROM
...and an SQL error because column employees_count doesn't exist and id and name are ambiguous.
The only thing that sort of works is this:
employers = Employer.include_counts.where(id: 1).select("employers.id, employers.name, count(employees.*) as employees_count")
...but that actually selects ALL the columns in employers, due to the scope clause again.
I also don't want that raw SQL leaking into my controller if I can avoid it. Is there a more idiomatic way to do this with Rails/Arel?
If I can't find another way to do the query, I'll probably create another scope or custom finder in the model, so that the controller code is cleaner. I'm open to suggestions for doing that as well, but I'd like to know if there's a simple way to reference computed aggregate columns like this as though they were any other column.

ActiveRecord: searchable concern

Problem
Suppose I have two models, both of which have a name field:
class Student < ActiveRecord::Base
end
class Teacher < ActiveRecord::Base
end
Now I want to find all the students and teachers who has the name 'Joe'. What I think to do:
name = 'Joe'
Student.where(name: name) + Teacher.where(name: name)
But things get ugly when I add other type of models which has also a name field and I want to search them:
name = 'Joe'
Student.where(name: name) + Teacher.where(name: name) + Manager.where(name: name) + ...
Question
Is there a better way to do this? Ideally what I would like to do, define a module Person and then just:
Person.where(name: name)
Note: Changing the schema is not possible for other reasons.
The solution may be to define your data models in a way that will lend themselves to this type of query. If you truly need to work on all "people" by name perhaps you should have a Person data model with roles or an attribute to define what "type" of person they are.
You're on the right track when you talk about defining a Person module. Look into database normalization to get an idea of how best to define your models.
Since teachers and students are both people, but have different roles, you should store both of them in the same model, but add a field which allows you to select just the teachers or just the students. Something like this (in pseudo-code):
Person
:name (string)
:role (string)
With this, you can write queries like "find all people named John who have the role of teacher", or "find all people named Sue with any role".
With this kind of model, you'd need to enforce values on that role field so that people couldn't type anything they wanted - otherwise, you'd end up with some records that say 'student' and others that say 'classmate', for example. (You could take that a step further and store roles within a separate table, but that's another discussion!)
You have to use inheritance with rails, you do it like this:
class Person < ActiveRecord::Base
# shared person methods
end
class Student < Person
end
class Teacher < Person
end
You have to also have a table named persons with a column named type(string).
After this your students and teachers are going to be in persons table (you can delete original tables).
And you can query all students and persons with Person.where(name: name) and will include all students and teachers. And you can do Student.all to get only students, like a regular model.

ActiveScaffold search a join table?

I have general field search in ActiveScaffold working. I'm not sure how to do this more complex searching.
I have two tables, Account and User, and I want to search emails and get back a list of matching accounts. The email field is in User, and Account has_many :users.
I am having trouble thinking through how the query should happen. Ideally I'd like to do something like this:
Account.where(email: 'search_term').all
or
User.where(email: 'search_term').includes(:account).all
If you want to search for data from one table and return results from(including) another, just add those foreign columns as virtual columns:
in User controller:
active_scaffold :user do |conf|
conf.search.columns << :email
conf.list.columns << :account
#...
end
That's it, no queries :)
if account column results appears code like <#23423.. it's because Active Scaffold can't tell how to describe that class records, so you tell it how you want in the model:
class Account << ActiveRecord::Base
....
def to_label
"cod: #{account_number}"
end

Ruby on rails activerecord joins - select fields from multiple tables

models:
#StatusMessage model
class StatusMessage < ActiveRecord::Base
belongs_to :users
default_scope :order => "created_at DESC"
end
#User Model
class User < ActiveRecord::Base
has_many :status_messages
end
In controller I want to join these two tables and get fields from both table. for example I want email field from User and status field from StatusMessage. When I use :
#status = User.joins(:status_messages)
Or
#status = User.includes(:status_messages)
It gives me only the user table data.
How can I implement this requirement?
You need to use includes here. It preloads data so you won't have another SQL query when you do #user.status_messages.
And yes you can't really see the effect: you need to check your logs.
First of all, I don't think it is possible (and reasonable) what you want to do. The reason for that is that the relation between User and StatusMessage is 1:n, that means that each user could have 0 to n status messages. How should these multitudes of attributes be included in your user model?
I think that the method joints in class ActiceRecord has a different meaning, it is part of the query interface. See the question LEFT OUTER joins in Rails 3
There are similar questions on the net, here is what I have found that matches most:
Ruby on Rails: How to join two tables: Includes (translated for your example) in the user a primary_status_message, which is then materialized in the query for the user. But it is held in one attribute, and to access the attributes of the status_message, you have to do something like that: #user.primary_status_message.status
When you use #status = User.includes(:status_messages) then rails eagerley loads the data of all the tables.
My point is when you use this User.includes(:status_messages) it will loads the data of status_messages also but shows only users table data then if you want first user status_messages then you have to #user.first.status_messages

What is the best way to do scoped finds based on access control rules in Rails?

I need to find an elegant solution to scoped finds based on access control rules. Essentially I have the following setup:
Users
Customers
AccessControl - Defines which user has access to another users data
Users need to be able to access not just their own customers but also shared customers of other users.
Obviously something like a simple association will not work:
has_many :customers
and neither will this:
has_many :customers, :conditions => 'user_id in (1,2,3,4,5)'
because the association uses with_scope and the added condition is an AND condition not an OR condition.
I also tried overriding the find and method_missing methods with the association extension like this:
has_many :customers do
def find(*args)
#get the user_id and retrieve access conditions based on the id
#do a find based on the access conditions and passed args
end
def method_missing(*args)
#get the user_id and retrieve access conditions based on the id
#do a find based on the access conditions and passed args
end
end
but the issue is that I don't have access to the user object / parent object inside the extension methods and it just does not work as planned.
I also tried default_scope but as posted here before you can't pass a block to a default scope.
Anyhow, I know that data segmentation and data access controls have been done before using rails and am wondering if somebody found an elegant way to do it.
UPDATE:
The AccessControl table has the following layout
user_id
shared_user_id
The customer table has this structure:
id
account_id
user_id
first_name
last_name
Assuming the the following data would be in the AccessControl table:
1 1
1 3
1 4
2 2
2 13
and so on...
And the account_id for user 1 is 13 I need to be able to retrieve customers that can be best described with the following sql statement:
select * from customers where (account_id = 13 and user_id = null) or (user_id in (1,3,4))
Sorry if I've completely missed the point here but I'm not 100% sure of what you want to do. Is AccessControl a relationship between User and Customer? If so looks like you just need to setup a many-to-many relationship.
class User
has_and_belongs_to_many :customers
# or this if you need to store meta data in the join table
has_many :customers
has_many :access_controls
has_many :accessible_customers, through => :access_controls
end

Resources