Admin Pages
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:
- Learning Rails example app code as of the end of Lesson 12
- Learning Rails example app code as of the end of Lesson 13
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.
Comments on This Lesson
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” = 1Question 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?
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