(first posted: Mar 25, 2007)
(8801 Reads)
keywords: acts_as_list acts_as_tree cms partial
Permalink
A Micro-CMS in Rails
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#