Reply to topic
Ruby on Rails tutorial - part 3
pmeserve
HostMySite Tech

Joined: 19 Mar 2004
Posts: 178
Reply with quote
Overview
This is part 2 of HostMySite's tutorial for building a blog in Rails. Please go through part 1 and part 2 before you start this section. In this section, we're going to add the ability to put our blog posts into categories. In the course of doing this we'll learn some more advanced Rails features

Create Categories

We need to start by generating some scaffolding around our categories table that we created at the beginning of the tutorial, using the following command:

../script/generate scaffold category

Once this is done you should be able to browse to the controller, http://[domain]/blog/categories/ and create a few example categories - give them whatever name/descriptions you'd like. Get at least 4-5 of them

Model

Now we're going to introduce an awesome new database relationship: HABTM!. That stands for "has and belongs to many" and is a perfect fit for what we want to do here. Basically we need to link two tables: posts, which each have an id, and categories, which each have an id. However neither has a direct link to the other - they can't, because each post can have more than one category, and each category can be assigned to many posts. To get around this we've got a third table: categories_posts - this table has entries with both a category_id and a post_id. We'll use it to find which categories belong to which posts. And with Rails, that's easy

Open up the posts model, models/post.rb and add the following text:

Code:
has_and_belongs_to_many :categories


And now open the categories model, models/category.rb, and setup the link the other way:

Code:
has_and_belongs_to_many :posts


Simple as that - Rails figures out that our join table will be called categories_posts by combining the names of the two existing tables(in alphabetical order) and now when we load a post, the associated categories will be loaded for us right along with it. Even more amazing, if we go back later and delete a category or post, Rails will automatically delete any associated records out of our join table - cleaning up for us

Controller

A very quick edit for the controller(controllers/posts_controller.rb) as well. Simply add this line to our new and edit methods:

Code:
@categories = Category.find(:all)


Doing this loads the full list of categories into a variable which we can use in the new and edit views - this will let us build a form field to allow users to select their post's categories

View

Almost done, but now we need to write a bit of more complicated code. Open up views/posts/_form.rhtml. We need to add a form field to allow people to select a category or categories. It needs to be a multi-select field to allow multiple selections. Here's the code we'll use:

Code:
<% unless @categories.empty? %>
        <%= link_to 'Category', {:controller => 'categories' }  %><br/>
        <select name="post[category_ids][]" multiple="multiple">
        <option></option>
        <%= options_from_collection_for_select( @categories, :id, :name) %><br/><br/>
        </select>
<% end %>


A few things worth explaining here:
* the field name, post[category_ids][] is important - we specified category_ids because of the special circumstance of this being a HABTM relationship
* we're using options_from_collection_for_select - passing it the object to build the list from, the id/name fields
----
Now in list view, views/posts/list.rhtml, we're going to add the following code under <%= nl2br(post.post) %>:

Code:
<%= render :partial => 'category', :locals => { :categories => post.categories } %>


We're using another partial, because we want to be able to display these categories in both the list and show views. Let's add the same code to the show view now too - you'll just need to change post.categories to @post.categories because of the way our variables are set in the two different views

Now we can create the partial, views/posts/_category.rhtml, with the following code:
Code:
<% unless categories.empty? %>
        <br/><br/><i>Categories:
        <%= @cats = ""
                categories.each {|cat| @cats += link_to (cat.name, :controller => 'categories', :action => 'show', :id => cat.id) + ", "  }
                @cats.chomp(", ") # returned to the browser because it's the last item in this section
        %>
        </i>
<% end %>


The code says: if post.categories isn't empty(i.e. if this post does have assigned categories), toss in some line breaks and a description. Then, we'll create an empty string [i]@cats[i], and loop through the [i]post.categories[i] array, giving each element the temporary name of [i]cat[i], and feeding it into a code block. Within the code block we'll concatenate @cats onto itself plus a link to the current category, and separate each name with a comma. Then at the very end, use [i]chomp[i] to yank off the trailing comma, and @cats is then displayed

----

You can now go back to your posts and add categories to any of your existing posts(or a new one). Also, if you have a post with existing categories and you edit it, the already set categories will be pre-selected in the form

Progress

So, we've put together a simple blog application: customized our layout, allowed user comments, added input validation, given users the ability to manage categories and categorize their posts...it's quite a lot. It may feel like it's been a good amount of coding as well, but let's see if that's true. Run the command:
rake stats

At the bottom of the output you'll see a tally of the total amount of code(and this includes scaffold code that Rails generated for us). Mine is: Code LOC: 122. This isn't going to count our code in views, but we can get an idea of that too with this command:
cat views/posts/* | grep "<%" | wc -l

Total there is 53, and for many of these lines, calling them "code" is generous - we're basically just outputting variables. Regardless, our total is 175 lines - incredible given how much we've already implemented. And much of this code we didn't even write - Rails generated it for us
jdrez


Joined: 22 Jun 2006
Posts: 2
Reply with quote
Wonderful tutorial! I followed it through and then modified and expanded it to fit my needs as a news publishing system!

The one issue I find is with the categories. For some reason, I can only put one post into each category. If I try to put another post into a category that already contains a post, I receive this error:

Code:
 ActiveRecord::StatementInvalid in PostsController#update

Mysql::Error: #23000Duplicate entry '4' for key 1: INSERT INTO categories_posts (`post_id`, `id`, `category_id`) VALUES (18, 4, 4)
pmeserve
HostMySite Tech

Joined: 19 Mar 2004
Posts: 178
Reply with quote
jdrez - That's odd, it looks like your "categories_posts" table has a primary key, which it shouldn't. Did you use the code from the migration to create it, i.e:

Code:
create_table "categories_posts", :id => false, :force => true do |t|
    t.column "post_id", :integer, :default => 0, :null => false
    t.column "category_id", :integer, :default => 0, :null => false
  end



Check in phpMyAdmin for your account and delete the "id" field for that table, and then everything should work properly
jdrez


Joined: 22 Jun 2006
Posts: 2
Reply with quote
That was the issue. I didn't use the migration, because for some reason it created a strange entry. I entered the tables and fields manually.

Thanks for the help!
jstedman


Joined: 29 Apr 2006
Posts: 27
Location: Newark, DE
Reply with quote
Some mysql management applications will create an id field in every table you create, whether you tell it to or not. I know MySQL Front does this, and I've seen at least one other MySQL GUI do it tho I can't recall which.
problem with options_from_collection section
kimbo


Joined: 28 Nov 2006
Posts: 2
Reply with quote
working against db2
no id in categories_post table, (used schema)
but I get the following error (I saw a similar error with another tutorial using the same type code)
Thoughts?

Showing app/views/posts/_form.rhtml where line #16 raised:

You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occured while evaluating nil.empty?

Extracted source (around line #16):

13:
14: <!--[eoform:post]-->
15:
16: <% unless @categories.empty? %>
17: <%= link_to 'Category', {:controller => 'categories' } %><br/>
18: <select name="post[category_ids][]" multiple="multiple">
19: <option></option>

Trace of template inclusion: /app/views/posts/edit.rhtml
pmeserve
HostMySite Tech

Joined: 19 Mar 2004
Posts: 178
Reply with quote
kimbo - Are you sure you have the line:

Code:
@categories = Category.find(:all)


in controllers/posts_controller.rb? Even if no categories exist, find(:all) should return an empty array, not a nil. nil should only happen if you don't have the code there to set the variable at all
fixed it
kimbo


Joined: 28 Nov 2006
Posts: 2
Reply with quote
That fixed it.
I had added it to the controllers/categories_controller.rb by mistake.

Thanks
Problem with dropdown list
fulvio


Joined: 27 Dec 2006
Posts: 5
Reply with quote
Hi,

I'm using the following:

Code:
<% unless @categories.empty? %>
<p>
    <select name="post[category_ids][]" id="post_category_title" class="sel_category">
        <option value="">Please Choose</option>
        <%= options_from_collection_for_select(@categories, :id, :name) %>
    </select>
    <label for="post_category_title">Category</label>
</p>
<% end %>


The problem is when I go to Edit a particular post that I've added a Category to, it doesn't actually populate the Category drop-down.

I know that the Categories are working fine as I can see them in my actual posts.

Any help would be greatly appreciated!
rob_hms


Joined: 20 Sep 2006
Posts: 4
Reply with quote
fulvio,

Is the categories select box empty or totally missing? If the latter, do you have the following in the "edit" method of controllers/posts_controller.rb controller:

Code:
@categories = Category.find(:all)


Is the categories select box being populated when you create a new post? If so, I'd imagine that the above code is simply missing from the edit method in your controller.

If the select box is there, but just empty, I would make sure the code matches the example 100%. Are all HTML tags properly nested / closed? You might try viewing the HTML course of the page to see if the categories are being listed anymore but not being rendered proerly by the browser.

Hope this helps!
fulvio


Joined: 27 Dec 2006
Posts: 5
Reply with quote
rob_hms wrote:
fulvio,

Is the categories select box empty or totally missing? If the latter, do you have the following in the "edit" method of controllers/posts_controller.rb controller:

Code:
@categories = Category.find(:all)


Is the categories select box being populated when you create a new post? If so, I'd imagine that the above code is simply missing from the edit method in your controller.

If the select box is there, but just empty, I would make sure the code matches the example 100%. Are all HTML tags properly nested / closed? You might try viewing the HTML course of the page to see if the categories are being listed anymore but not being rendered proerly by the browser.

Hope this helps!


Still no luck! Crying or Very sad

This is my edit method.

Code:
  def edit
    @post = Post.find(params[:id])
    @categories = Category.find(:all)
  end

This is my html source when editing /blog/edit/27 the selected category should be "Ruby".

Code:
<p>
    <select name="post[category_ids][]" id="post_category_title" class="sel_category">
      <option value="">Please Choose</option>
      <option value="1">General</option>
      <option value="2">Books</option>
      <option value="3">Developers</option>
      <option value="4">Links</option>
      <option value="5">Science</option>
      <option value="6">Quantum Physics</option>
      <option value="7">Site</option>
      <option value="8">Cinema</option>
      <option value="9">Television</option>
      <option value="10">Ruby</option>
      <option value="11">Rails</option>
      <option value="12">Code</option>
      <option value="13">DVD</option>
    </select>
    <label for="post_category_title">Category</label>
</p>
pmeserve
HostMySite Tech

Joined: 19 Mar 2004
Posts: 178
Reply with quote
fulvio - You're right that the code posted doesn't work the way you'd expect. Back when I wrote this I was quite a newbie at Rails, and the categories feature wasn't thought-through properly

Here's my question: do you want to have the ability for multiple categories assigned to a post, or only one category per post? The basic solutions will be:

for multiple categories:
setup select field as a MULTIPLE select, and then pass in all the IDs of the categories belonging to the posts into the helper method for pre-selection

for only one category:
switch the HABTM relationship to post belongs_to category, and then simply send the helper function @post.category

If you'd like a bit more complete code posted, let me know which of the two you're looking to accomplish
fulvio


Joined: 27 Dec 2006
Posts: 5
Reply with quote
pmeserve wrote:
fulvio - You're right that the code posted doesn't work the way you'd expect. Back when I wrote this I was quite a newbie at Rails, and the categories feature wasn't thought-through properly

Here's my question: do you want to have the ability for multiple categories assigned to a post, or only one category per post? The basic solutions will be:

for multiple categories:
setup select field as a MULTIPLE select, and then pass in all the IDs of the categories belonging to the posts into the helper method for pre-selection

for only one category:
switch the HABTM relationship to post belongs_to category, and then simply send the helper function @post.category

If you'd like a bit more complete code posted, let me know which of the two you're looking to accomplish


For the time being I'd like just one category per post. I appreciate all your help, if you could provide me some more code that would be great! Thanks.
pmeserve
HostMySite Tech

Joined: 19 Mar 2004
Posts: 178
Reply with quote
OK, you're going to want to start by editing the post & category models, because you're changing the relationship between them. The category will now be:
Code:
has_many :posts


and the post will now be:
Code:
belongs_to :category


To reflect this change in the database, you can remove the categories_posts table and instead just add a field category_id to the posts table

in views/posts/_form.rhtml, try this:

Code:
<% unless @categories.empty? %>
        <%= link_to 'Category', {:controller => 'categories' }  %><br/>
        <select name="post[category_id]">
        <option></option>
        <%= options_from_collection_for_select( @categories, :id, :name, @post.category) %><br/><br/>
        </select>
<% end %>


You'll also need to edit the places where the categories for a post are displayed. It should be much simpler, e.g.
Code:
<%= "categorized as #{@post.category.name}" if @post.category %>


This should work. Let me know if not, I'll need to dig up a complete copy of the code to test with. I don't have one with me at the moment
fulvio


Joined: 27 Dec 2006
Posts: 5
Reply with quote
pmeserve wrote:
OK, you're going to want to start by editing the post & category models, because you're changing the relationship between them. The category will now be:
Code:
has_many :posts


and the post will now be:
Code:
belongs_to :category


To reflect this change in the database, you can remove the categories_posts table and instead just add a field category_id to the posts table

in views/posts/_form.rhtml, try this:

Code:
<% unless @categories.empty? %>
        <%= link_to 'Category', {:controller => 'categories' }  %><br/>
        <select name="post[category_id]">
        <option></option>
        <%= options_from_collection_for_select( @categories, :id, :name, @post.category) %><br/><br/>
        </select>
<% end %>


You'll also need to edit the places where the categories for a post are displayed. It should be much simpler, e.g.
Code:
<%= "categorized as #{@post.category.name}" if @post.category %>


This should work. Let me know if not, I'll need to dig up a complete copy of the code to test with. I don't have one with me at the moment


I've done everything except I get an Application error (Rails) when I navigate to e.g. /blog/show/33.

ActionView::TemplateError (undefined method `categories' for #<Post:0xb793b0f8>) on line #2 of app/views/blog/show.rhtml:
1: <%= render :partial => 'post', :object => @post %>
2: <%= render :partial => 'category', :locals => { :categories => @post.categories } %>
3:
4: <%
5: unless @post.comments.length < 2


Obviously something wrong with this guy:

Code:
<%= render :partial => 'category', :locals => { :categories => post.categories } %>
Ruby on Rails tutorial - part 3
You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum
All times are GMT  
Page 1 of 2  

  
  
 Reply to topic