views:

82

answers:

2

I have created a table in my Ruby on Rails application that I am building called Tags. It is a blog application so I allow the user to associate tags with a post and do this through a :posts, :has_many => tags and Tag belongs_to :post association.

Now that I have my Tags table I am trying to see how I would render the view such that it displays the tag and tag count. (it should be noted that I am trying to render this in the /views/posts/index.html.erb file).

For instance, if there are 10 entries in the Tag table for tag_name Sports. How can I display Sports (10) in the view. I am not looking to do this for a specific tag but rather, somehow search the table, combine like tags and display a list of all tags with counts next to them. (I really want these to be a link to a list of posts that contain that tag but I learned early on only to ask one question at a time).

Hope that makes sense.

UPDATE FOR COMMENTS

view

<% @tag_counts.each do |tag_name, tag_count| %>
    <tr>
      <td><%= link_to(tag_name, posts_path(:tag_name => tag_name)) %> </td>
      <td>(<%=tag_count%>)</td>
    </tr>
<% end %>

postsController:

def index
    @tag_counts = Tag.count(:group => :tag_name)
    @posts = Post.all :order => "created_at DESC"



    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @posts }
      format.json { render :json => @posts }
      format.atom
    end
  end
+3  A: 

Do the following:

Tag.count(:group => :name).each do |tag_name, tag_count|
  puts "tag_name=#{tag_name}, tag_count=#{tag_count}"
end

You might improve the performance if you add an index on thename column in the tags table.

To display the posts associated with a tag name do the following:

In the controller method set the count hash:

Set @tag_counts in the controller action associated with the view displaying the tag names.

@tag_counts = Tag.count(:group => :tag_name)

In the view show each tag as a link:

<% @tag_counts.each do |tag_name, tag_count| %>
  <%= link_to(tag_name, posts_path(:tag_name => tag_name)) %> (<%=tag_count%>)
<% end %>

The link points to the index method of your PostsController. Each link has a tag_name parameter.

In the index method of PostsController:

class PostsController < ApplicationController
  def index
    @tag_counts = Tag.count(:group => :tag_name)
    conditions, joins = {}, nil
    unless (params[:tag_name] || "").empty?
      conditions = ["tags.tag_name = ? ", params[:tag_name]]
      joins = :tags
    end
    @posts=Post.all(:joins => joins, :conditions => conditions)
  end
end

Edit Updated the code to change the name field to tag_name.

KandadaBoggu
This might be stupid question, but it looks like this is the code that I would use in the Tag controller to define count. If that is the case, how do I render in the view? <%= h(tag.count) %>?
bgadoci
I have updated my answer. Take a look.
KandadaBoggu
Man, thanks so much for taking the time to do this. I will check it out first thing tomorrow. Again, I really really appreciate it.
bgadoci
Hey man, getting close I think. When you sat to set the count hash with @tag_counts = Tag.count(:group => :name), you say to put in the controller. I have tried in the app controller and both post and tags and not working. Can you give me a little more detail around where to put that and how (like do I do a def tag_count blah end?
bgadoci
you said you're gonna use this in `/views/posts/index.html.erb`, so you have to put `@tag_counts = Tag.count(:group => :name)` in the `index` method of `PostsController`
j.
Set the `@tag_counts` in the controller action associated with the view displaying the tag names.
KandadaBoggu
Ok, got the view to work. Had to make two changes (that also may be why the linking isn't working). First had to add 'do' and second had to change :name to :tag_name because I am an idiot and named my column in Tags table :tag_name instead of just :name. When I click the link for the tag in the view, it successfully reads http://localhost:3000/posts?tag_name=foobar but then gets whacked. I've updated my question with code. Can't tell you how much I appreciate all your help.
bgadoci
K just updated code but didn't add the piece that KandadaBoggu said to put in the index method because I think it may need to be changed. When I do put it in I get undefined local variable for tag_name.
bgadoci
There was a typo in my code(`:tag_name` instead of `tag_name` while referring to the `params` key)
KandadaBoggu
Updated the code to add the changes required for your edit.
KandadaBoggu
OMG, it worked. Thank you thank you thank you.
bgadoci
Any reason it would screw up the /posts view in general? Now getting this error when going back to /posts: You have a nil object when you didn't expect it!You might have expected an instance of Array.The error occurred while evaluating nil.empty?
bgadoci
I'm guessing I need add something after the .empty? in the controller, just not sure how to do it. Sorry, this is it I promise.
bgadoci
AH! fixed it. Just changed .empty? to .blank?
bgadoci
The `empty?` works on Nil, String, Array, Hash etc. I wonder why it didn't work for you. Which version of Rails are you using?
KandadaBoggu
Rails 2.3.5 and actually I am having some problems with .blank? Now, in my index view of posts, only posts that have been tagged show up. I will change it back to .empty? and trouble shoot from there as I think the other is causing problems.
bgadoci
I have another issue that I will upload as second question that may be .empty? related as when I delete a post, the view that I have set for recent comments errors out because it can't find the post that the comment is associated with. So getting a better understanding of this problem should help me with that as well.
bgadoci
You have to destroy the associated comments when you delete a post. You can do that by changing your `comments` association in `Post` model, i.e. `has_many :comments, :dependent => :destroy`
KandadaBoggu
Don't do this. You'll have to crawl the db for every single Tag, which would never scale. Rails has a feature built-in for caching this number.
Adam Lassek
Count displayed is not per post but across posts. Based on user's current schema design it is not possible to configure a cache counter for unique tag count across posts. Ideally user should use `acts_as_taggable_on` for this OR introduce a HABTM relationship between posts and tags.
KandadaBoggu
Right, the real reason he's having trouble is because he's using the wrong relationship. Post/Tag are many-to-many; I addressed this in my answer. Once the logic is structured correctly, introducing a counter_cache is easy.
Adam Lassek
Yes. I agree. I usually tend to give the solution to the problem posed and also suggest a alternative approach. This time, I didn't and I should have.
KandadaBoggu
A: 

First -- you appear to have your associations wrong. Tags and Posts are a many-to-many association, not one-to-many.

I strongly suggest looking at one of the acts_as_taggable implementations to do this for you. Otherwise you can create a has_many :through association if you wish, but this would be reinventing the wheel.

Calling count() on a collection for every page load is a very bad idea, because that will make you hit the database for every tag; a very expensive operation at scale. Also, the number of Posts with a given Tag is not a figure you need to calculate at request time, so this approach is both expensive and unnecessary.

Rails has a built-in feature called counter_cache that will take care of this for you, by caching the number of related records in an integer field and updating it whenever new records are created. Here's how you set it up:

Create a migration for for the tags table:

def up
  add_column :tags, :taggings_count, :integer, :default => 0

  Tag.reset_column_information
  Tag.all.each do |t|
    Tag.update_counters t.id, :taggings_count => t.taggings.length
  end
end

def down
  remove_column :tags, :taggings_count
end

And change the belongs_to association:

class Tagging < ActionRecord::Base
  belongs_to :tag, :counter_cache => true
  belongs_to :post
end

If you're building your own Tagging system you'd wire up the remaining two models like so:

class Post < ActiveRecord::Base
  has_many :taggings
  has_many :tags, :through => :taggings
end

class Tag < ActiveRecord::Base
  has_many :taggings
  has_many :posts, :through => :taggings
end

But, again, it'd be much easier to use a pre-built solution.

From now on, every time you create a new Post association to a Tag through the Tagging model, it will update the taggings_count column for you automatically.

In your views, you can display the count while iterating like any other column:

<%= link_to "#{tag.name} (#{tag.taggings_count})", posts_path(:tag_name => tag.name) %>

Some further reading:
ActiveRecord has_many :through associations
ActiveRecord association basics (:counter_cache discussed in section 4.1.2.4)

Adam Lassek