lost password?

home
•  reviewramp
•  rails +
•  javascript
•  webdev
•  django
•  xaraya
•  xamp
•  musings

rss
Tag this page
   

ยป Blogs that link here
last modified: Apr 23, 2007
(first posted: Mar 25, 2007)
(13942 Reads)
keywords: acts_as_list acts_as_tree cms partial
Permalink

A Micro-CMS in Rails

Here's a quick tutorial that builds a little page manager for Rails apps, which can be used for About pages and such. Lets you arrange pages in a hierarchy, and change the order position. Includes a little side menu.

Get Started

To get started generate your Rails app and database, if you haven't already, something like this:

$ rails myapp
$ cd myapp 
$ mysqladmin -u root -p create myapp_development 

Edit config/database.yml as needed, e.g. add your password, and then test the db connection (the db:migrate does nothing yet but should not generate any errors):

$ rake db:migrate

Create Pages Resource

We'll start with the RESTful resource scaffold, and define the model as an ordered list (position field), and a tree (parent_id foreign key references the same table).

$ script/generate scaffold_resource page name:string title:string body:text updated_on:date parent_id:integer position:integer 
$ rake db:migrate 

Edit app/models/page.rb, add the following:

acts_as_tree :order => :position
acts_as_list :scope => :parent_id 

Replace app/views/pages/show.rhtml with something like the following:

<h1><%= @page.title %></h1>
<div>
    <%= @page.body %>
</div>

Create Some Example Pages

Link to http://0.0.0.0:3000/pages, and add pages such as the following (only need to fill out these fields, leave the others blank/default):

Name: about
Title: About Us
Body: This is the About page.

Name: contact
Title: Contact Us
Body: This is the Contact us page.

Name: privacy
Title: Privacy Policy
Body: This is the privacy policy page.

Name: history
Title: Our History
Body: This page describes our history.

Check results by linking to http://0.0.0.0:3000/pages/1 , /2 , etc.

Route to Page by Name

Numbers are ugly in a url so lets use the page name for id instead, so we can link to http://0.0.0.0:3000/pages/about , /contact , etc.

Edit app/controllers/pages_controllers.rb, change the beginning of def show to:

def show
@page = Page.find_by_name(params[:id]) # GET/pages/name
@page ||= Page.find(params[:id]) # GET/pages/id

If you want to go even further, and avoid the /pages/name so you can go to http://mysite.com/about or even about.html, add this to routes.rb:

# any named page
map.connect ":id", :controller => "pages", :action => "show"
map.connect ":id.html", :controller => "pages", :action => "show"

Handle Bad Page ID's

If you specify an unknown page id in the url (whether id number or name), you get a Rails error page. Lets handle that as a 404 exception in the show action,

def show
begin
@page = Page.find_by_name(params[:id]) # GET/pages/name
@page ||= Page.find(params[:id])
rescue
redirect_to_url "/404.html"
else

...
  end 
end 

If you don't want to lose the layout, you could create a page named "404error" (for example) with the "not found" message (then dont need that else clause):

  rescue
@page = Page.find_by_name('404error')
end

...

Make a Layout (with Side Menu)

You might want to make a nicer page layout, if you don't have one. The main point here is to add <%= yield :sidemenu %> that we'll use next. If you're starting from scratch try this: edit views/layouts/pages.rhtml as follows:

In the <head> section replace the <title> tag with:

<title><%= (@page.nil? || @page.title.nil?) ? "My Site" : @page.title %></title>
 

And replace the default <body> section with this (using inline styles to simplify this tutorial):

<body style="background-color:#ddd">
  <div style="min-height:600px; margin:0 20px; background-color:white">
    <div style="height:80px; background-color:#88A; text-align:center; padding:1em">
      <h1>Welcome to My Site</h1>
    </div>
    <div>
      <div style="float:left; background-color:#888">
        <%= yield :sidemenu %>
      </div>
      <div style="margin-left:150px">
        <p style="color: green"><%= flash[:notice] %></p>
        <%= yield :layout %>
      </div>
    </div>
  </div>
</body>

Add Side Menu

Now we'll add a side menu containing all the pages, in proper position (we'll let you edit the position next).

In pages_controller.rb, in def show add the following line

  def show
@page = Page.find_by_name(params[:id]) # GET/pages/name
@page ||= Page.find(params[:id])
@pages = Page.find( :all, :order => :position)

Add to views/pages/show.rhtml:

  <% content_for(:sidemenu) do %> 
<h3>Pages:</h3>
<ul>
<% for item in @pages %>
<li><%= link_to item.title, page_url(:id => item.name) %></li>
<% end %>
</ul>
<% end %>

Now when you view a page, the menu of all pages appears on the left.

Edit Page Positions

Next we let you modify the relative position of pages in the index list, by adding "up" and "down" links, with RESTful syntax like /pages/1;higher and /pages/1;lower.

Edit pages_controller.rb, add the following actions:

 

# PUT /pages/1;higher
def higher
page = Page.find(params[:id])
unless page.nil?
if page.first?
page.move_to_bottom
else
page.move_higher
end
end
redirect_to pages_url
end

# PUT /pages/1;lower
def lower
page = Page.find(params[:id])
unless page.nil?
if page.last?
page.move_to_top
else
page.move_lower
end
end
redirect_to pages_url
end

In pages_controller, to sort the index list by position, change first line of def index to

@pages = Page.find(:all, :order => :position) 

Edit routes.rb, add custom actions:

map.resources :pages,
:member => { :higher => :put,
:lower => :put }

Edit views/pages/index.rhtml, replace position cell:

<td><%=h page.position %></td> 

with:

<td>
<%=h page.position %>
<%= link_to 'Up', higher_page_path(page), :method => :put %>
<%= link_to 'Dn', lower_page_path(page), :method => :put %>
</td>

Edit and Show Tree Hierarchy

Initially, we already defined acts_as_tree in the model. Now lets use it.

Edit view/pages/edit.rhtml, for the "parent" field, replace

<%= f.text_field :parent_id %>

with

<%= select("page", "parent_id", Page.find(:all).collect {|p| [ p.name, p.id ] }, { :include_blank => true } ) %>

Link to /pages and edit the examples pages "history" and "contact" so both have the "about" page as their parent.

Wouldn't it be nice to show the pages hierarchy in the index list? Unfortunately I haven't found an easy way to sort the pages as parent1, child1a, child1b, parent2, child2a, etc. So instead we'll use partials and recursively show the children.

To start, the index action will only pass the list of root pages to the template. So change the first line of def index as follows:

  def index
@pages = Page.find( :all, :conditions => ['parent_id IS NULL'], :order => :position )

Edit views/pages/index.rhtml and replace the entire <% for page in @pages %> ... <% end%> loop with this line:

<%= render(:partial => "index_item", :collection => @pages )%>

Then create a file views/pages/_index_item.rhtml with the following:

<tr>
<td>
<%= prefix ||= nil %>
<%=h index_item.name %>
</td>
<td><%=h index_item.title %></td>
<td><%=h index_item.body %></td>
<td><%=h index_item.updated_on %></td>
<td><%=h index_item.parent_id %></td>
<td>
<%=h index_item.position %>
<%= link_to 'Up', higher_page_path(index_item), :method => :put %>
<%= link_to 'Dn', lower_page_path(index_item), :method => :put %>
</td>
<td><%= link_to 'Show', page_path(index_item) %></td>
<td><%= link_to 'Edit', edit_page_path(index_item) %></td>
<td><%= link_to 'Destroy', page_path(index_item), :confirm => 'Are you sure?', :method => :delete %></td>
</tr>
<% unless index_item.children.nil? %>
<%= render(:partial => "index_item", :collection => index_item.children, :locals => {:prefix => '|--'} )%>
<% end %>

Changes from the index.rhtml are highlighted. Note the recursive trick here, after a root page row is output, if the page has children then we call the same partial for the children. But in that case, we append a prefix "|--" a crude way of showing the tree graph.

 

Modified Side Menu

The left side menu still shows all the pages. Lets change it to show the root pages, and current section pages separately.

In pages_controller.rb, in the show action, change the @pages= assignment, to:

@pages = Page.find( :all, :conditions => ['parent_id IS NULL'], :order => :position ) 

and add this line after it,

@relatives = @page.relatives

And then edit models/page.rb to return the list of relatives (this page, uncles, and children):

  def relatives
c = parent.nil? ? [self] : [parent] + parent.children
c += children
end

In views/pages/show.rhtml, add this to the <% content_for(:sidemenu) do %> block:

  <% unless @relatives.nil? || @relatives.size <= 1 %>
<h3>In This Section:</h3>
<ul>
<% for item in @relatives %>
<li><%= link_to item.title, page_url(:id => item.name) %></li>
<% end %>
</ul>
<% end %>

 


Other Stuff

You'll want to improve this, and adapt it to your own application. For instance, the side menu might be nested menu instead. You might also want to add a WYSIWYG editor for the page body, or some other markup. Naturally, you'll want to add some authentication so visitors only access the show action, while administrators can create, update, and delete pages.

 

A Micro-CMS in Rails

Posted by: Tom on March 28, 2007 04:28 AM
Change @section = @page.relatives to @relatives = @page.relatives Tom

#

Re: A Micro-CMS in Rails

Posted by: linoj on March 28, 2007 08:24 AM
oops, right. Fixed it in the article. thx. It makes you think though, which would be more semantically correct, to name the variable "relatives" or "section"? :)

#

A Micro-CMS in Rails

Posted by: Carmelyne on April 26, 2007 03:53 AM
nice tutorial. ;p

#

A Micro-CMS in Rails

Posted by: Arik Jones on October 06, 2007 08:53 PM
What about urls? Have you been able to reflect the hierarchy in the url as well? Ex: parent/child/grand-child

#

A Micro-CMS in Rails

Posted by: linoj on October 07, 2007 09:50 AM
Arik, good question. I havent tried it myself, but I'd probably route to the top level page, and permit optional level2 and level3 names as params, something like /:id/:level2/:level3 . Then in the show action, find the child of params[:id] with name params[:level2], and find the child of level2 with name params[:level3]. According to the Agile book (2nd edition- may'07, p 401) in routes.rb, you can add :level2 => nil, :level3 => nil to map.connect to make them optional.

#

A Micro-CMS in Rails

Posted by: linoj on October 11, 2007 09:40 PM

A Micro-CMS in Rails

Posted by: tresero on January 01, 2008 10:39 PM
For rails 2.0 script/generate resource Page

#

A Micro-CMS in Rails

Posted by: tresero on January 02, 2008 01:50 AM
OOPs I meant script/generate scaffold Page name:string title:string body:text parent_id:integer position:integer I also added some constraints for not null title and name. How hard would this be to change to add arbitrary routes from the same controller (i.e. /pages/page /articles/page etc.)

#

A Micro-CMS in Rails

Posted by: liam on April 25, 2008 06:15 PM
Wonderful - a complete newbie and you have made me understand everything! One question/help needed: my "up" and "dn" links do not seem to work - I get the following error...... Unknown action No action responded to higher ----- what am I doing wrong?

#

A Micro-CMS in Rails

Posted by: liam on April 25, 2008 06:23 PM
Sorry!!! My fault - I was missing the last "end" in the pages_controller.rb

#

showing hierarchy

Posted by: Scott on August 23, 2008 07:17 PM
1) Thank you for this awesome walkthrough/tutorial/example set! 2) One limitation of the acts_as_free design here is that it only shows one level of hierarchy (no matter now many levels you have). Here's a three-line block that fixes this: it replaces a two-line block in _index_item.rhtml - the middle line is the new one. (May be an easier way to do it - I made it more explanatory.) <% unless index_item.children.nil? %> <% if (prefix.nil?) then prefix = '|--' else prefix = prefix + '|--' end %> <%= render(:partial => "index_item", :collection => index_item.children, :locals => {:prefix => prefix} )%>

#

Performance issues

Posted by: Scott on September 23, 2008 12:37 AM
It's worth noting that this code does a single SQL query for every page (to see if it has children). This makes rendering the page list very slow. It would make sense to create an array at the top with all of the pages with children, and check that, rather than doing the index_item.children.nil? check on every line in the recursion. If I do this, I'll provide the code.

#

Performance Improvement

Posted by: ScottRu on September 26, 2008 11:03 AM
FYI, as above, I've posted code for reducing the number of SQL queries and massively speeding up the page. It's at http://scottru.com/2008/09/26/vaporbase-micro-cms-performance-improvement/ . Again, thanks for the great work!

#

A Micro-CMS in Rails

Posted by: Fortevast on February 26, 2009 06:38 AM
Hi dude.. Im having a doubt.. just like in windows environment when we open mycomputer (windows+E) it will display in frame format, in left side there will be tree with folders. when we click on that "+" symbol, it changes to "-" and then a tree will be displayed right? can we get the same "+" and "-" thing in ruby?????? when we click "+", then only we must get the sub folders of that folder??, if we can do please mail me at mca.jayanth [at] gmail.com :::::::: replace [at] with @:::::::::: pls make this clarified. Thanks in advance.

#

Post a new comment

How many days in a week?

Name :