I've added an Advance Search to my Book App using this tutorial. Everything works fine, but now I am trying to find a Book by its Tags.
I got the advance search to work if the user enters one Tag into the :keywords text_field.
Is there a way to search various tags by splitting the keyword string with commas?
(ex: fun, kid stories, action)
Would allow me to search books with fun OR kids stories OR actions.
How can I search multiple tags via a comma separated string?
Note: I created a search method that I think could help, but I am not sure how to combine it with the single keyword search.
MODEL
class Book < ActiveRecord::Base
has_many :book_mappings
has_many :tags, through: :book_mappings
end
class BookMapping < ActiveRecord::Base
belongs_to :book
belongs_to :tag
end
class Tag < ActiveRecord::Base
has_many :book_mappings
has_many :books, through: :book_mappings
end
class Search < ActiveRecord::Base
def books
#books ||= find_books
end
def find_books
books = Book.order(:name)
###This works for a single word but NOT if I have multiple tags separated by commas
books = books.joins(:tags).where("tags.name like ?", "%#{keywords}%") if keywords.present?
books
end
def search(keywords)
return [] if keywords.blank?
cond_text = keywords.split(', ').map{|w| "name LIKE ? "}.join(" OR ")
cond_values = keywords.split(', ').map{|w| "%#{w}%"}
all(:conditions => (keywords ? [cond_text, *cond_values] : []))
end
end
VIEWS
<%= form_for #search do |f| %>
<div class="field">
<%= f.label :keywords %><br />
<%= f.text_field :keywords %>
</div>
<% end %>
Here is a simple solution. Just add a like statement for each keyword.
To filter books with all the tags
if keywords.present?
books = books.joins(:tags)
keywords.tr(' ','').split(',').each do |keyword|
books = books.where("tags.name like ?", "%#{keyword}%")
end
end
To filter books with any of the tags
if keywords.present?
books = books.joins(:tags)
keyword_names = keywords.split(', ')
cond_text = keyword_names.map{|w| "tags.name like ?"}.join(" OR ")
cond_values = keyword_names.map{|w| "%#{w}%"}
books = books.where(cond_text, *cond_values)
end
Related
I have a has_many, through: relationship between Projets and Categories using a Categorization model. A Projet belongs_to a Client.
class Client < ApplicationRecord
has_many :projets
end
class Category < ApplicationRecord
has_many :categorizations, dependent: :destroy
has_many :projets, through: :categorizations
end
class Categorization < ApplicationRecord
belongs_to :category
belongs_to :projet
end
class Projet < ApplicationRecord
belongs_to :client
has_many :categorizations, dependent: :destroy
has_many :categories, through: :categorizations
end
For a specific category I'd like to list all the projets, grouped by the client. e.g.
(for category_id = 3)
Client A
Projet 1
Projet 2
Client B
Projet 3
Client C
Projet 4
So far I can get this working, but only by using two queries (one of which is very inefficient (n+1 problem).
This is the code
def listing
#projets_clients = Projet
.select("client_id")
.includes(:client)
.joins(:categorizations)
.where(categorizations: { category: #category })
.group("client_id")
#clients = []
#projets_clients.each do |p|
#clients << Client.includes(:projets).find(p.client_id)
end
end
If anyone can suggest a better approach I'd love to learn how to optimise this as I haven't been able to find a better way myself.
Thanks.
There are a few different ways to do this. For complex queries I sometimes find it easier to write and execute straight SQL. However for your case, depending on data size, you could just eager load the data and turn it into a hash.
Note: When I was testing this code I used projects instead of projets.
#category = Category.includes(projects: [:client]).find(2)
#projects_by_client = #category.projects.group_by(&:client_id)
# In your view
<%- #projects_by_client.each do |client_id, projects| %>
<%= projects.first.client.name %>
<%- projects.each do |project| %>
<%= project.name %>
<% end %>
<% end %>
A more fleshed out solution might use full sql with a query object and presenter object. I spun up a quick project using the below code and the output is what you are looking for.
# app/controllers/clients_controller.rb
class ClientsController < ApplicationController
def show
result = ClientQuery.call(params[:id])
#presenter = ClientPresenter.new(result)
end
end
# app/services/client_query.rb
class ClientQuery
class << self
def call(client_id)
sql_query(client_id)
end
protected
def sql_query(client_id)
ActiveRecord::Base.
connection.
execute(
sanitized_sql_statement(client_id)
)
end
def sanitized_sql_statement(client_id)
ActiveRecord::Base.send(
:sanitize_sql_array,
[
sql_statement,
client_id
]
)
end
def sql_statement
<<-SQL
SELECT
c.id AS client_id,
c.name AS client_name,
p.name AS project_name
FROM
clients c
INNER JOIN
projects p ON p.client_id = c.id
INNER JOIN
categorizations cz ON cz.project_id = p.id
INNER JOIN
categories ct ON ct.id = cz.category_id
WHERE
ct.id = ?;
SQL
end
end
end
# app/presenters/client_presenter.rb
class ClientPresenter
attr_reader :clients
def initialize(data)
#clients = {}
process_sql_result(data)
end
private
def process_sql_result(data)
data.each do |row|
client_id = row['client_id']
#clients[client_id] ||= { client_name: row['client_name'] }
#clients[client_id][:projects] ||= []
#clients[client_id][:projects] << row['project_name']
end
end
end
# app/views/show.html.erb
<%- #presenter.clients.each do |client_id, client_presenter| %>
<h1><%= client_presenter[:client_name] %></h1>
<ul>
<%- client_presenter[:projects].each do |project_name| %>
<li><%= project_name %></li>
<% end %>
</ul>
<% end %>
This is of course just one of many ways you could go about getting your data in a single query and presenting it.
I am modifying a Documents table from using three columns (article1, article2, and article3) to one (articles) which has a string of comma-separated IDs stored in it (i.e., 23,4,33,2). That's all working well, but I'm trying to adjust the functions that read the three columns to read the one and I'm getting rather stuck.
In the model I have:
scope :all_articles, lambda {|p| where(:page => p) }
In the controller I have this:
#articles = (1..3).to_a.map { |i| Article.all_articles(i).reverse }
And in the view:
<% #articles.each_with_index do |a, i| %>
<%= a[i].name %>
<% end %>
It's just a bit beyond me at this point.
Cheers!
It's usually not good practice to put store the ids in a column like you have done. It is better to break that relationship out into a Has and Belongs to Many relationship. You set it up in your models like this:
class Document < ActiveRecord::Base
#...
has_and_belongs_to_many :articles
#...
end
class Article < ActiveRecord::Base
#...
has_and_belongs_to_many :documents
end
Then you will create a join table that ActiveRecord will use to store the relationships.
create_table :articles_documents, :id => false do |t|
t.integer :article_id
t.integer :document_id
end
add_index :articles_documents, [:article_id, :document_id], unique: true
This will allow you to query a lot more efficiently than you are currently doing. For example, to find all documents that have some article id. You would do:
#documents = Document.joins(:articles).where("articles.id = ?", some_article_id)
Or if you want to query for a document and return the articles with it:
#documents = Document.includes(:articles).where(some_conditions)
first time poster. I am trying to sort a table of users using the Ransack gem and Kaminari for pagination. When I use name, id, etc. sorting works but when I try an association with posts_count, sorting breaks and won't work. Note: in the view, 'u.posts.count' work correctly. I have tried custom scopes in the users model, and creating custom objects for the search params but nothing seems to work. I think I am having trouble either in the default scope or the #search object not having the data. Need help!
Here are some relevant snippets:
models/user.rb
has_many :posts, :dependent => :destroy
models/post.rb
belongs_to :user
default_scope :order => 'post.created_at DESC'
controllers/users_controller.rb
def index
#title = "User Index"
#search = User.search(params[:q]) # Ransack
#total_users = User.all.count
# .per(10) is the per page for pagination (Kaminari).
#users = #search.result.order("updated_at DESC").page(params[:page]).per(10) #This displays the users that are in the search criteria, paginated.
end
views/users/index.html.erb
..
<%= sort_link #search, :posts_count, "No. of Posts" %> #Sort links at column headers
..
<% #users.each do |u| %> #Display everything in the table
<%= u.posts.count %>
<% end %>
You can add a scope to your User model:
def self.with_posts
joins(:posts).group('posts.id').select('users.*, count(posts.id) as posts_count')
end
and use it like this:
#search = User.with_posts.search(params[:q]) # Ransack
then, you can treat posts_count like any other attribute.
I found a solution:
Controller:
def index
sql = "users.*, (select count(posts.id) from posts\
where posts.user_id = users.id) as count"
#search = User.select(sql).search(params[:q])
if params[:q] && params[:q][:s].include?('count')
#users = #search.result.order(params[:q][:s])
else
#users = #search.result
end
.......
end
View:
<th><%= sort_link #search, :count, "posts count" %></th>
I'm using Rails 3.0.7 with awesome_nested_set and I'm trying to create a nested form which will allow me to enter a category and sub categories all in one create form.
Here is my category model, controller & form
category.rb
class Category < ActiveRecord::Base
acts_as_nested_set
end
categories_controller.rb
class CategoriesController < InheritedResources::Base
def new
#category = Category.new
3.times { #category.children.build(:name => "test") }
end
end
form
= form_for #category do |f|
-if #category.errors.any?
#error_explanation
%h2= "#{pluralize(#category.errors.count, "error")} prohibited this category from being saved:"
%ul
- #category.errors.full_messages.each do |msg|
%li= msg
.field
= f.label :name
= f.text_field :name
%p Sub categories
= f.fields_for :children do |child|
= child.text_field :name
.actions
= f.submit 'Save'
The problem here is that I only end up with one sub category in my form and it doesn't have name set to 'test' so I don't believe that it's actually the child of the category showing here.
What am I missing here?
Can this be done?
Is there an easier way?
Update
If I change my form to the following then it displays three sub categories each with name set to 'test'. This will not save correctly though.
%p Sub categories
- #category.children.each do |sub|
= f.fields_for sub do |child|
= child.label :name
= child.text_field :name
%br
Found my answer and wrote a wiki page about it here:
https://github.com/collectiveidea/awesome_nested_set/wiki/nested-form-for-nested-set
I have a HABTM-relation between the models "Snippets" and "Tags". Currently, when i save a snippet with a few tags, every tag is saved as a new record.
Now i want to check if a tag with the same name already exists and if that´s the case, i don´t want a new record, only an entry in snippets_tags to the existing record.
How can i do this?
snippet.rb:
class Snippet < ActiveRecord::Base
accepts_nested_attributes_for :tags, :allow_destroy => true, :reject_if => lambda { |a| a.values.all?(&:blank?) }
...
end
_snippet.html.erb:
<% f.fields_for :tags do |tag_form| %>
<span class="fields">
<%= tag_form.text_field :name, :class => 'tag' %>
<%= tag_form.hidden_field :_destroy %>
</span>
<% end %>
Ok, i´m impatient… after a while i found a solution that works for me. I don´t know if this is the best way, but i want to show it though.
I had to modify the solution of Ryan Bates Railscast "Auto-Complete Association", which handles a belongs_to-association to get it working with HABTM.
In my snippet-form is a new text field named tag_names, which expects a comma-separated list of tags.
Like Ryan, i use a virtual attribute to get and set the tags. I think the rest is self-explanatory, so here´s the code.
View "_snippet.html.erb"
<div class="float tags">
<%= f.label :tag_names, "Tags" %>
<%= f.text_field :tag_names %>
</div>
Model "snippet.rb":
def tag_names
# Get all related Tags as comma-separated list
tag_list = []
tags.each do |tag|
tag_list << tag.name
end
tag_list.join(', ')
end
def tag_names=(names)
# Delete tag-relations
self.tags.delete_all
# Split comma-separated list
names = names.split(', ')
# Run through each tag
names.each do |name|
tag = Tag.find_by_name(name)
if tag
# If the tag already exists, create only join-model
self.tags << tag
else
# New tag, save it and create join-model
tag = self.tags.new(:name => name)
if tag.save
self.tags << tag
end
end
end
end
This is just the basic code, not very well tested and in need of improvement, but it seemingly works and i´m happy to have a solution!