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)
Related
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
I created two tables in my app (Rails 3):
def change
create_table :articles do |t|
t.string :name
t.text :content
t.timestamps
end
create_table :tags do |t|
t.string :name
t.timestamps
end
create_table :articles_tags do |t|
t.belongs_to :article
t.belongs_to :tag
end
add_index :articles_tags, :article_id
add_index :articles_tags, :tag_id
end
I want to be able to search for articles based on tags in two ways:
Articles with ANY of the given tags (union)
Articles with ALL of the given tags (intersection)
So, in other words, something that allows me to do this this:
tag1 = Tag.create(name: 'tag1')
tag2 = Tag.create(name: 'tag2')
a = Article.create; a.tags << tag1
b = Article.create; b.tags += [tag1, tag2]
Article.tagged_with_any(['tag1', 'tag2'])
# => [a,b]
Article.tagged_with_all(['tag1', 'tag2'])
# => [b]
The first one was relatively easy. I just made this scope on Article:
scope :tagged_with_any, lambda { |tag_names|
joins(:tags).where('tags.name IN (?)', tag_names)
}
The problem is the second. I have no idea how to do this, in ActiveRecord or SQL.
I figure that I might be able to do something icky like this:
scope :tagged_with_all, lambda { |tag_names|
new_scope = self
# Want to allow for single string query args
Array(tag_names).each do |name|
new_scope = new_scope.tagged_with_any(name)
end
new_scope
}
but I'm betting that's crazy inefficient, and it just smells. Any ideas about how to do this correctly?
As you said, that scope is crazy inefficient (and ugly).
Try with something like this:
def self.tagged_with_all(tags)
joins(:tags).where('tags.name IN (?)', tags).group('article_id').having('count(*)=?', tags.count).select('article_id')
end
The key is in the having clause. You may also want to have a look at the SQL division operation between tables.
I'm trying to find an elegant (standard) way to pass the parent of a polymorphic model on to the view. For example:
class Picture < ActiveRecord::Base
belongs_to :imageable, :polymorphic => true
end
class Employee < ActiveRecord::Base
has_many :pictures, :as => :imageable
end
class Product < ActiveRecord::Base
has_many :pictures, :as => :imageable
end
The following way (find_imageable) works, but it seems "hackish".
#PictureController (updated to include full listing)
class PictureController < ApplicationController
#/employees/:id/picture/new
#/products/:id/picture/new
def new
#picture = imageable.pictures.new
respond_with [imageable, #picture]
end
private
def imageable
#imageable ||= find_imageable
end
def find_imageable
params.each do |name, value|
if name =~ /(.+)_id$/
return $1.classify.constantize.find(value)
end
end
nil
end
end
Is there a better way?
EDIT
I'm doing a new action. The path takes the form of parent_model/:id/picture/new and params include the parent id (employee_id or product_id).
I'm not sure exactly what you're trying to do but if you're trying to find the object that 'owns' the picture you should be able to use the imageable_type field to get the class name. You don't even need a helper method for this, just
def show
#picture = Picture.find(params[:id])
#parent = #picture.imagable
#=> so on and so forth
end
Update
For an index action you could do
def index
#pictures = Picture.includes(:imagable).all
end
That will instantiate all 'imagables' for you.
Update II: The Wrath of Poly
For your new method you could just pass the id to your constructor, but if you want to instantiate the parent you could get it from the url like
def parent
#parent ||= %w(employee product).find {|p| request.path.split('/').include? p }
end
def parent_class
parent.classify.constantize
end
def imageable
#imageable ||= parent_class.find(params["#{parent}_id"])
end
You could of course define a constant in your controller that contained the possible parents and use that instead of listing them in the method explicitly. Using the request path object feels a little more 'Rails-y' to me.
I just ran into this same problem.
The way I 'sort of' solved it is defining a find_parent method in each model with polymorphic associations.
class Polymorphic1 < ActiveRecord::Base
belongs_to :parent1, :polymorphic => true
def find_parent
self.parent1
end
end
class Polymorphic2 < ActiveRecord::Base
belongs_to :parent2, :polymorphic => true
def find_parent
self.parent2
end
end
Unfortunately, I can not think of a better way. Hope this helps a bit for you.
This is the way I did it for multiple nested resources, where the last param is the polymorphic model we are dealing with: (only slightly different from your own)
def find_noteable
#possibilities = []
params.each do |name, value|
if name =~ /(.+)_id$/
#possibilities.push $1.classify.constantize.find(value)
end
end
return #possibilities.last
end
Then in the view, something like this:
<% # Don't think this was needed: #possibilities << picture %>
<%= link_to polymorphic_path(#possibilities.map {|p| p}) do %>
The reason for returning the last of that array is to allow finding the child/poly records in question i.e. #employee.pictures or #product.pictures
I have the two following models associated:
class Post < ActiveRecord::Base
belongs_to :language
end
class Language < ActiveRecord::Base
has_many :posts
end
From a view I have a link to filter the posts by language:
<div id="english"><%= link_to "English", {:controller => 'posts', :action => 'search_result', :language => "english"} %></div>
The model language has a variable name:string which is the one i am using to make the active record query.
The doubt i have is how i can make this query from the post controller to retrieve the right posts which has a field: language.name == "english".
I tried this:
#posts = Post.all(:conditions => ["language.name = ?", params[:language]])
and also this:
#posts = Post.where(:language.name => params[:language])
Hope i have explained well the issue, i am a quite newbie yet. Ah! i would also know what would it be better in this case to use: "all" or "where" ??.
Thanks a lot in advance.
You need to do a join: http://guides.rubyonrails.org/active_record_querying.html#specifying-conditions-on-the-joined-tables
If I have understood your models/database structure correctly, the ActiveRecord call should looks something like:
Post.joins(:language).where('languages.name' => params[:language])
Hope that helps.
PS. The where call is the preferred method these days.
Hi I am new to rails and I would like to know what is the best way who save dependent objects in an HBTM relation.
Specifically, I have two classes Post and Tag
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :posts
end
I have a migration to create the joining table
class AddPostsTagsJoinTable < ActiveRecord::Migration
def self.up
create_table :posts_tags, :id => false do |t|
t.integer :post_id
t.integer :tag_id
end
end
def self.down
drop_table :postss_tags
end
end
All is good up to here
So I have a PostsController from which I handle the creation, updates and deletes for the posts, and I want to encapsulate the Tags so that the creation is via the PostsController... like so:
class PostsController < ApplicationController
#... code removed for brevity
def create
#post = current_user.posts.build(params[:post])
if #post.save
tag_names = params[:post][:tags].strip.split(' ')
tag_names.each do |t|
#see if the tag already exists
tag = Tag.find_by_name(t);
if tag.nil?
#post.tags.create!(:name => t)
else
#post.tags << tag #just create the association
end
end
flash[:success] = "Post created."
redirect_to(user_posts_path(current_user.username))
else
#user = current_user
render 'new'
end
end
end
I am not sure how I should handle the creation of my Tag(s) because if I just call
#post.tags.create!(:name => t)
this will create duplicate records in the Tags table (even when :uniq => true is specified in the model).
So to avoid the duplication I see if a tag is already present and then add it like this
tag = Tag.find_by_name(t);
if tag.nil?
#post.tags.create!(:name => t)
else
#post.tags << tag #just create the association
end
Is this the way it's supposed to be done?
This seems expensive (especially 'cause it's in a loop) so I am wondering if there is another "cleaner" way to do this? (pls forget the DRY'ing up of the action and so on)
Is there a clean way to create my Tags without having to manually check for duplicates?
thank you in advance for your help!
You can save tags attribute of post if automatically by adding accepts_nested_attributes_for to Post model
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags
accepts_nested_attributes_for :tags
end
The next step is to output tags fields inside post form.