Table of Contents

View Screencast (Quicktime)
Right-click and choose Save As to save file to your computer


Sign Up Now!

If you aren’t already receiving our course lessons via email, sign up now to be sure you don’t miss anything.

Every few days, we’ll send you an email with a link to the next episode, plus a list of additional resources for advancing your knowledge.

There’s no cost and no obligation. And we’ll never share your email address with any third party.


We’ll send you the first lesson right away.


Want to help spread the word? We’d be grateful if you would include a link to the course in your blog, web site, or emails.

Admin Pages

12 comments

Goals

In this lesson, we implement the actual administrative dashboard using improvements we make to the mini-CMS we are building. We finish up with an improvement to our navigation code so we can build the tabbed interface more dynamically.

Setup

We begin with the code with which we ended Lesson 12. These zip files contain the beginning and ending states of the code:

Admin Pages

Of course, it is tiresome to keep typing the URLs manually to get to our administrative pages. Let’s create an admin dashboard page that connects us to these sub-pages. Let’s also make the admin page easy to reach when we are appropriately logged in.

Make the Admin page attribute

If we are going to use our baby-CMS to implement this page, what is missing? We need to know when a page is an admin type page so we can protect it properly.

Make a migration to add an admin attribute to pages, and then set some default values in the migration to keep things tidy.

class AddAdminPageAttribute < ActiveRecord::Migration
  def self.up
    add_column :pages, :admin, :boolean

    @pages = Page.find(:all)
    @pages.each do |page|
      page.update_attribute(:admin, false)
    end

  end

  def self.down
    remove_column :pages, :admin
  end
end

Now, we need to update the Page Admin html so we can see/edit the new admin attribute:

First, index.html.erb gets a couple of new table entries:

<h1>Listing pages</h1>

<table>
  <tr>
    <th>Name</th>
    <th>Title</th>
    <th>Body</th>
    <th>Admin?</th>
  </tr>

<% for page in @pages %>
  <tr>
    <td><%=h page.name %></td>
    <td><%=h page.title %></td>
    <td><%=h page.body %></td>
    <td><%= page.admin? ? "TRUE" : "FALSE" %></td>
    <td><%= link_to 'Show', page %></td>
    <td><%= link_to 'Edit', edit_page_path(page) %></td>
    <td><%= link_to 'Destroy', page, :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>
<% end %>
</table>

<br />

<%= link_to 'New page', new_page_path %>

Then add a snippet to show.html.erb to see the Admin value:

<p>
   <b>Admin?</b><br />
   <%= @page.admin? ? "TRUE" : "FALSE" %>
</p>

Update both edit.html.erb and new.html.erb with a snippet to use a check box for the state of the admin attribute (put this in before the submit):

<p>
  <b>Admin?</b><br />
  <%= f.check_box :admin %>
</p>

We should be all set. Check it out and create a page with the admin bit set. Can you see it when you are logged in? What about when you aren’t? Oops!

Update the viewer controller to handle special pages

Of course, we don’t filter in anyway for admin pages, so anyone can see a page marked with the Admin bit. Let’s fix that by protecting admin viewable pages by updating the viewer controller:

def show
  @page = Page.find_by_name(params[:name])
  login_required if @page.admin?
end

We are leveraging the login_required function provided to us by the restful_authentication plugin. If the requested page has the admin value set to true, we’ll check to see if the user is logged in with login_required.

Add an admin dashboard page in the DB

Finally, we can create a page using the mini-CMS’ Page Admin. Make one now and we’ll call it “admin”. Make the page contents as follows:

    <a href="/pages">"Page Admin"</a>
    <br>
    <a href="/users">"User Admin"</a>

Test it. Check it logged in and logged out. It works!

Make layout dynamic

It is getting really tiresome to keep updating our simple navigator, since we need to access the admin pages regularly, let’s take this opportunity to make a new tab appear for each page automatically.

Update the application layout to support rendering tabs dynamically. Here is the new navbar div:

<div id='navbar'>
    <ul>
        <% @tabs.each do |page| -%>
            <li><%= link_to page.title, view_page_path(page.name) %></li>
        <% end -%>
        <li><% if logged_in? %>
                    <%= link_to "Log Out", logout_path %>
                <% else %>
                    <%= link_to "Log In", login_path %>
                <% end %>
        </li>
    </ul>
<div>

We need to add a before_filter to application.rb to load up pages into that @tabs variable:

before_filter :get_pages_for_tabs

def get_pages_for_tabs
  if logged_in?
    @tabs = Page.find(:all)
  else
    @tabs = Page.find(:all, :conditions => ["admin != ?", true])
  end
end

This won’t deal well with lots of pages, so in the future we’ll improve this solution with the notion of page groups, sub-navigation, and other tricks.

Wrapping up

Try out the new navigation at localhost:3000. You may want to tweak the titles of the pages to make the tabs look better. We are reusing the same data as is used for the page titles in the title bar of the browser. A better solution over time may be to add a separate attribute so we can keep SEO friendly titles.

We finally have support for “administrative” pages in our simple CMS, so we can create the dashboard using our own technology!

Coming up

Next time, we’ll add the ability to use Textile markup in our CMS pages (via a plugin) and start playing with AJAX to make the UI a little easier to use.


Add a Comment

Have a comment or question about this lesson? Add it here.






Comments on This Lesson

From: Bohdan S.       Date: 06/22/08 03:03 AM

Subject: Sorry,

originally code supposed to look like that:

Migrating to AddAdminPageAttributeToPages (4)
  SQL (0.046000)   ALTER TABLE pages ADD "admin" boolean
  SQL (0.032000)   VACUUM
  Page Load (0.000000)   SELECT * FROM pages
  Page Update (0.000000)   UPDATE pages SET "created_at" = '2008-06-21 19:24:35',
    "name" = 'home',
    "title" = 'Welcome to the Learning Rails Sample App',
    "body" = '<h1>Welcome to our home page</h1>',
    "updated_at" = '2008-06-22 12:54:58',
    "admin" = 'f'
  WHERE "id" = 1

From: Bohdan S.       Date: 06/22/08 03:03 AM

Subject: A typo? Or a bug?

When running ‘add_column’ migration, I’ve noticed this SQL in server’s cosole log:

Migrating to AddAdminPageAttributeToPages (4) SQL (0.046000) ALTER TABLE pages ADD “admin” boolean SQL (0.032000) VACUUM Page Load (0.000000) SELECT * FROM pages Page Update (0.000000) UPDATE pages SET “created_at” = ‘2008-06-21 19:24:35’, “name” = ‘home’, “title” = ‘Welcome to the Learning Rails Sample App’, “body” = ‘

Welcome to our home page

’, “updated_at” = ‘2008-06-22 12:54:58’, “admin” = ‘f’ WHERE “id” = 1

Question is: doest it really suppose to set “admin”=’f’ ??... Running Rails 2.0.2 with sqlite3, on WinXP

From: Michael Slater       Date: 06/17/08 11:23 PM

Subject: Code improvements

Clemens,

Thanks for your suggestions. I agree that adding the default value would be a good idea.

Using update_all is also a good idea, as a matter of principle, though the resources used are so small for this application that I don’t think it makes any real difference.

From: Clemens Kofler       Date: 06/17/08 11:11 AM

Subject: Little recommendation regarding migrations

Christopher,

great lesson as usual (I watch them even though I’m quite experienced with Rails). I have one small thing to mention: In the migration where you added the :admin column, you should probably set the column’s :default to false. This way, it updates all existing records to be non-admin pages (at least in MySQL) and future pages will automatically be non-admin unless you specifically set them to be an admin page. Moreover, I think you shouldn’t fetch all records and iterate over the collection if all you’re doing is updating one attribute without any further conditions. You could use Page.update_all for that which is especially great if you already have dozens of objects because it doesn’t fetch them from the database – thus saving a lot of resources.

Regards, - Clemens

From: Christopher Haupt       Date: 06/12/08 09:21 PM

Subject: RE: Migration Errors with add_column migration

It is really important that the file name and the migration class name match up. The file name should be all lower case, start with a number, and each word separated by an underscore. So in our example above, the class name is AddAdminPageAttribute so the migration file name should be either 123_add_admin_page_attribute.rb (prior to Rails 2.1) or 20080605123456_add_admin_page_attribute.rb (in Rails 2.1 or higher). The numbers are an incrementing sequence number (in the former) or a timestamp (in the latter).

Can you confirm that the file names and class names within the migration are identically named, and just differ in their formatting and initial number?

-c

From: Darius del Rosario       Date: 06/12/08 04:04 AM

Subject: Migration error too

I am having the same problems as the previous post (Subject: Cannot migrate add_column). What could be happening here? I even used your lesson files to follow along with the tutorial so that I’d be ‘on the same page’ so to speak with you.

Could the version of Rails be the problem, or the development machine? I’m using Windows XP SP2 and the latest Rails (v. 2.1.0). I understand you used v. 2.0.2 in the tutorials.

Great tutorials by the way! I can’t believe you’re giving these away for free. Thanks.

From: fil       Date: 06/09/08 06:18 PM

Subject: Cannot migrate add_column

I cannot get the migration to execute properly. I keep receiving a rake error. I’ve copied the code directly from this page.

Error is: rake aborted! uninitialized constant AddAdminPageAttributeToPages

With trace:
  • Invoke db:migrate (first_time)
  • Invoke environment (first_time)
  • Execute environment
  • Execute db:migrate rake aborted! uninitialized constant AddAdminPageAttributeToPages /usr/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:266:in `load_missing_constant’ /usr/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:453:in `const_missing’ /usr/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:465:in `const_missing’ /usr/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/inflector.rb:257:in `constantize’ /usr/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/core_ext/string/inflections.rb:148:in `constantize’ /usr/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/migration.rb:386:in `migration_class’ /usr/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/migration.rb:363:in `migration_classes’ /usr/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/connection_adapters/sqlite_adapter.rb:351:in `inject’ /usr/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/migration.rb:359:in `each’ /usr/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/migration.rb:359:in `inject’ /usr/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/migration.rb:359:in `migration_classes’ /usr/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/migration.rb:339:in `migrate’ /usr/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/migration.rb:307:in `up’ /usr/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/migration.rb:298:in `migrate’ /usr/local/lib/ruby/gems/1.8/gems/rails-2.0.2/lib/tasks/databases.rake:85 /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:546:in `call’ /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:546:in `execute’ /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:541:in `each’ /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:541:in `execute’ /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:508:in `invoke_with_call_chain’ /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:501:in `synchronize’ /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:501:in `invoke_with_call_chain’ /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:494:in `invoke’ /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1931:in `invoke_task’ /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1909:in `top_level’ /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1909:in `each’ /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1909:in `top_level’ /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1948:in `standard_exception_handling’ /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1903:in `top_level’ /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1881:in `run’ /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1948:in `standard_exception_handling’ /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1878:in `run’ /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/bin/rake:31 /usr/local/bin/rake:19:in `load’ /usr/local/bin/rake:19

Any advice on how to debug?

From: Michael Slater       Date: 05/20/08 08:08 AM

Subject: Processing time question

Lindsay, I understand your concern, and while I haven’t benchmarked this specifically, I’ll bet that the time to build the tabs is small and that the processing load is insignificant unless you’re running a very high-traffic site. In that case, you can easily optimize this by using the built-in Rails caching system, so the result is cached and not computed each time. Unfortunately, that is beyond the scope of this tutorial.

From: lindsay       Date: 05/19/08 11:11 AM

Subject: responsiveness...

quick question… i understand making the layout dynamic, but should we worry about the processing time? it seams that if we needed to dynamically build tabs for each page each time we browse to one it could cause some problems when trying to scale. i know this isn’t production ready and is just a demo… just curious. the lessons are great.

thanks, lindsay

From: Bob Walsh       Date: 05/07/08 09:09 AM

Subject: Your lessons are excellent!

Just a quick comment to say your lessons are among the best I’ve seen in 25 years developing. Please! Keep up the good work!

From: Christopher Haupt       Date: 05/05/08 05:17 PM

Subject: RE: Project Assistance

John: We may eventually get to the more advanced topic of search in a future episode. For now, you might want to check out Ferret, Solr, or one of the other search engines out there. We have a few links on BuildingWebApps.com if you search for “search”. With respect towards listing out database contents of the CMS app from within a CMS generated page, we aren’t quite there yet. The steps you would need to take in the mean time are to either create a custom page (e.g. a view on the pages controller, for instance), or if you are feeling like diving in deeper, look at supporting embedded Ruby within the pages themselves.

From: John       Date: 05/05/08 11:11 AM

Subject: Project Assistance

i am creating a database however, i would like to know how to install a search engine & i have a customer tab that i want to open “listing customer” within the customer frame. however, i am having some problems with this how do i do this?