A complete turbogears spoliation: howto build a wiki in 20 minutes (well I did not time it)

Content

Purpose

For a certain purpose (that you do not want to know) I had to compare Turbogears and Ruby on Rails (which I’ll call RoR from now on).

As I’m not a Python guy, the simplest approach for me was to take the Turbogears tutorial and try to re-implement it with RoR, to be able to compare both of them.

Note that although I will follow the Turbogears tutorial plan and chapters, I’ll also introduce some typical RoR concepts where ever they do make sense. For example, in the Turbogears tutorial, the site layout is achieved by copying templates. In this particular case, I’ll introduce RoR layout as this i their exact purpose.

The Quickstart

Let’s create the directory hierarchy of our wiki:

rails wiki
   create
   create  app/controllers
   create  app/helpers
   create  app/models
   create  app/views/layouts
   ....
   create  doc/README_FOR_APP
   create  log/server.log
   create  log/production.log
   create  log/development.log
   create  log/test.log

And enter our playground:

cd wiki

Now Serving: Number 1

We can start our web server:

script/server

This will start either Webrick or Lighttpd (depending on your machine OS and the presence or not of lighttpd). Note that on my gentoo I need to add /usr/sbin to my path in order for RoR to detect my lighttpd installation.

Let’s have a look at http://localhost:3000:

Let’s create a wiki!

RoR follows the MVC paradigm (Model-View-Controller). To begin with we have to choose our model and then our controller.

Our wiki will be made of Pages (our model), each of them will have a name and content.

Our model will be ultimately hosted in a database table, so we have the choice to either write directly SQL code to create this table (pages), which columns will be name and content, or to work with RoR schemas and migrations. This last solution has 2 benefits:

Pointing to a database

Let’s begin by showing to RoR our developmenet database. To do so, you just have to edit config/database.yml which will be the only configuration file you’ll have to edit.

In this example I’ll use SQLite as the development database, and will name the database file wiki.sqlite:

development:
  adapter: sqlite
  database: db/wiki.sqlite

We’re also going to create our model, Page:

script/generate model Page
  exists  app/models/
  exists  test/unit/
  exists  test/fixtures/
  create  app/models/page.rb
  create  test/unit/page_test.rb
  create  test/fixtures/pages.yml
  create  db/migrate
  create  db/migrate/001_create_pages.rb

Creating the Database

Regarding the database management and more precisely the schema management, the simplest is to let Rails handle it for us. To achieve this, we’re going to use migrations, which are, more or less the way Rails handles the different versions of the schema (you can see this as increments from one version of the schema to the next one).

When we created the Page model, you perhaps noted the following line:

create  db/migrations/001_create_pages.rb

Rails created for us our first migration, which should describe how to go from the state “empty schema” to the state “schema describing a pages table”.

Let’s have a lok at db/migrations/001_create_pages.rb:

    class CreatePages < ActiveRecord::Migration
      def self.up
        create_table :pages do |t|
          # t.column :name, :string
        end
      end

      def self.down
        drop_table :pages
      end
    end
  

This file describes the schema of a pages table … using Ruby. The neat advantage being that you can now generate SQL code that will target the specific database type you choose (mySQL, SQLite, Oracle,...).

self.up method describes how to go from schema version n to schema version n+1 … et self.down express the exact contrary.

Let’s describe our model:

   def self.up
     create_table :pages do |t|
       t.column :name, :string, :limit => 30
       t.column :content, :text
     end
   end
 

Now, we just defined a table, named pages, in our database, which has two columns:

We now just need to ask Rails to apply our migration to the database:

   > rake migrate
   (in /home/sleeper/tmp/wiki)
   == CreatePages: migrating
   =====================================================
   -- create_table(:pages)
      -> 0.0438s
   == CreatePages: migrated (0.0440s)
   ============================================
 

Our first tabel is now created. Note that Rails also created the db/schema.rb file, which do represent our whole schema in its current version. And that’s all: our database has been created, with the right schema:

    > sqlite db/wiki.sqlite
    SQLite version 2.8.16
    Enter ".help" for instructions
    sqlite> .dump pages
    BEGIN TRANSACTION;
    CREATE TABLE pages ("id" INTEGER PRIMARY KEY NOT NULL, "name" 
    varchar(30), "content" text);
    COMMIT;
    sqlite>
  

Restart your web server, by hitting control-C in the terminal you started script/server before, and re-issuing the script/server command.

Let’s display a wiki page!

To display a wiki page we must use a controller, and a (or several) view(s). Our controller will be named (to be original) Wiki.

It will (for the time being) only expose 1 method: index. This method wile called whenever the client request for http://localhost:3000/wiki/index.html

This method (index) should help to display either the first page (named PageFront) or any pages which name has been given in parameter.

Thus http://localhost:3000/wiki/index.html will display the FrontPage page, while http://localhost:3000/wiki/index.html?name=Foo will display the Foo page.

So let’s create a Wiki controller, the index method and associated view:

script/generate controller Wiki index
  exists  app/controllers/
   exists  app/helpers/
   create  app/views/wiki
   exists  test/functional/
   create  app/controllers/wiki_controller.rb
   create  test/functional/wiki_controller_test.rb
   create  app/helpers/wiki_helper.rb
   create  app/views/wiki/index.rhtml

Note that we can get rid of the index parameter in the previous line, and add ourselves the index function to the Wiki controller, in the app/controllers/wiki_controller.rb file:

class WikiController < ApplicationController
  def index
    @page = Page.find_by_name( params[:name] || 'FrontPage' )
  end
end

So: in the index function we’re looking in our database (through the Page model) a page which name matches the name given by user, or FrontPage if no name was given.

We just have to create the index.html page, by editing the app/views/wiki/index.rhtml template:

     <div style="float:right; width: 10em"> 
       <p>Viewing <span><%= @page.name %></span></p>
       <p>You can return to the <a href="/wiki">FrontPage</a>. </p>
     </div> 
     <div><%= textilize(@page.content) %></div> 
   

That is: we’re referencing the @page variable the index method just sets, and display both the name and content of the associated page. Note that the content is first passed through the textilize function that will transfrom any Textile markup into plain HTML (this will work if RedCloth is installed and can be required).

So let’s try to load the http://localhost:3000/wiki page and we get ... an error:

This error is just due to the fact that our database is empty ... So let’s use script/console to create the FrontPage page:

    script/console 
    Loading development environment.
    >> Page.new( :name => 'FrontPage', :content => 'Welcome to *my* front page').save
    => true
    >>     
    

Reload the page in your browser, and … tada !!!

It works !!!

How do we make a better page?

OK. Every wiki have a way to edit the current page. So we’re going to add an “Edit this page” link to our current page.

First thing: when we’ll push the “edit this page” link we’re going to be redirected to a page which style is similar to our current page (and even to our wiki style).

Instead of copying the index.rhtml page and modifying it, we’re going to use a layout.

Layout is a good way in RoR to follow the DRY(Don’t Repeat Yourself) concept. Basically this is a shell that will receive our new pages. This allow to create and maintain the general design of our site in only one place.

So let’s do it, and check it works for our index.html page. The layout to use for a given controller is named after the controller name. We also want that the “Viewing [page name]” stuff to be available on all pages of our wiki. So placing it in the layout is a good idea… but we must pay attention that sometime we won’t look at a given page, but edit it … So we need to parameterize this action too …

So let’s create the app/views/layouts/wiki.rhtml layout:

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
    <head>
      <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
      <title> Wiki </title>
      <%= stylesheet_link_tag "wiki" %>
    </head>

    <body>

      <div id="sidebar" style="float:right; width: 10em"> 
        <% if @page and @page.name %>
          <p><%= @page_action || "Viewing" %> <span  id="name"><%= @page.name %></span> </p>
        <% end %>
         <p>You can return to the <a href="/wiki">FrontPage</a> </p>
      </div>            

      <div id="main">
        <%= yield  %>
      </div>

    </body>
    </html>
  

We’re just asking the inclusion of the wiki.css stylesheet (which has to be created in the public/stylesheets directory), and using the yield stuff, that indicate to RoR where to insert our pages. We’re also parameterizing the action to display: by default it will be “Viewing” but can be overridden by defining the @page_action in the template.

Let’s create a simple public/stylesheets/wiki.css:

  body {
    font-family: sans-serif;
  }

  div#sidebar {
     border: 2px solid red;
  }

  span#name {
     color: blue;
  }
 

and modify index.rhtml to look like:

 <div><%= textilize(@page.content) %></div> 
 

Now let’s reload our page:

We can now think about adding a new page. The page where we’re going to edit a given page is simple: it only contain a form, pointing to the save action, with a text area and a submit button. So let’s create the template, named app/views/wiki/edit.rhtml:

  <%= form_tag :action => "save" , :id => @page %>
    <%= text_area 'page', 'content' %>
    <%= submit_tag %>
  <%= end_form_tag %>

and add an “edit this page” link pointing to the edit.html page to the end of our page in the index.rhtml template:

<%= link_to "Edit this page", { :action => "edit", :name => @page.name } %>

We also need an edit method in our controller:

def edit
  @page_action = "Editing" 
  @page = Page.find_by_name( params[:name] )
end

Now, let’s reload our page and click on the “Edit this page” link:

Saving our edits

So we saw that our form is pointing to a save action. This action should be implemented by our controller, and should commit our changes to the database and warn us if something goes wrong.

Let’s add it to the app/controllers/wiki_controller.rb file:

   def save
     @page = Page.find( params[:id])
     @page.content = params[:page][:content]
     if @page.save
       flash[:notice] = "Page successfully saved" 
     else
       flash[:notice] = "Unable to save edited page" 
     end
     redirect_to( :action => "index", :name => @page.name )
   end
   

So to sum-up: we first get the original page we were editing (using its database id: params[:id]), and update it’s content with the content as sent by the form. We then save it to the database (using the save method of our model), and update the flash notice to reflect success or failure to user. We then redirect him to the index.html page, with our page as parameter.

Let’s just add something on our layout to display the flash notice:

   <div id="flash"><%= flash[:notice] %></div>
   

and something to wiki.css too:

   div#flash {
      background: #E0F8F8;
      border: 2px solid #244;
      width: 80%;
   }      
   

and we’re done:

Friendlier URLs

So, to display the page Foo, one must ask for the /wiki/index.html?name=Foo URL, which is not very sexy. The best would be something like /wiki/Foo. Due to the fact RoR uses the different tokens of an URL, this will end in trying to send the Foo message to our Wiki controller, which does not exist.

The simplest is to define a method_missing function for our controller: it will be called whenever an unknown message is send to our controller … and we can just try to redirect it to the index function with the right parameter :

  def method_missing( *args )
    redirect_to( :action => 'index', :name => args[0] )
  end
  
Note taht we can achieve a similar result by hacking the config/routes.rb file.

What about WikiWords?

Wikiwords are words clunk together in a typical CamelCase. Every wiki will automatically create links for these words when encountering them, pointing to a page with the same name.

This seems quite simple: we just have to build the right regexp, and edit the content on the flight, adding the required links.

First the regexp. CamelCase can be described as “an upper case letter followed by any word constituent, repeated at least 1 time”, which exprimed as a regexp leads to:

CamelCase = Regexp.new( '\b((?:[A-Z]\w+){2,})' )

Now, we just have to make each of these words an hyperlink, either in the content of the page, or in it’s textilized version.

   def index
     @page = Page.find_by_name(  params[:name] || 'FrontPage' )
     @page.content.gsub!( CamelCase, '<a href="/wiki/\1">\1</a>' )
   end
   

and test it:

Hey, where’s the page?

So now we also have to check for non-existing page. For example, clicking on the previous WikiWords link will produce an error, as our wiki tries to look for a page named WikiWords in the database, and this page does not exist.

Our approach to this problem will be the following: if index fails to find the requested page, it will redirect the request to the edit method. The edit method in return will be modified to create a new page named after the request if it’s not in the database, and then edit it. Note that we use the create method of our model instead of the new method: create creates object in memory and saves it to database while new only creates it in memory, defferring commit to the database to the save method.

     def index
       @page = Page.find_by_name(  params[:name] || 'FrontPage' )

       if @page.nil?
         redirect_to( :action => 'edit', :name => params[:name] )
       else
         @page.content.gsub!( CamelCase, '<a href="/wiki/\1">\1</a>' )
       end

     end

     def edit
       @page_action = "Editing" 
       @page = Page.find_by_name( params[:name] ) || Page.create( :name => params[:name] )
     end
     

Adding a page list

Being a nice wiki, we want to be able to list all the available page to our users. So we’re going to create a new action, called list, and it’s associated view named list.rhtml.

So in app/controllers/wiki_controller.rb add the following:

    def list
      @pages = Page.find_all
    end
  

list only gets all the pages available in the databse, and set the @pages to this list. Create the file app/views/wiki/list.rhtml containing the following code:

    <h1>All Of Your Pages</h1> 
    <ul> 
      <% for page in @pages %>
      <li><a href="/wiki/<%= page.name %>" ><%= page.name %></a></li> 
      <% end %>
    </ul> 
  

and also a link to our new functionality in our layout wiki.rhtml, for example in the sidebar:

   <p>View the <%= link_to "complete list of pages", :action => 'list' %></p>
 

I.e. we’re asking for inclusion of a link tag (a in HTML) that will redirect us to the action list (i.e. the /wiki/list url).

Reload the index page on your browser, and click on the new link:

You want AJAX? We got AJAX!

OK. The purpose of this section is to modify our previously created list of page. Instead of having this list displayed in it’s own page, we want it to automagically appear, bellow our “View the complete list of pages” link. Using AJAX and AJAX support in RoR, this is very simple.

First of all, modify the wiki.rhtml to include the prototype javascript library (which comes with RoR). So add:

<%= javascript_include_tag "prototype" %>

just bellow the <%= stylesheet_link_tag "wiki" %> line.

We now have to modify the previously inserted link (“View complete list of pages”) to something like:

    <p>View the <%= link_to_remote "complete list of pages", 
                                   :update => "pagelist",
                                   :url => { :action => 'list'}

                 %></p>
    <ul id="pagelist"></ul>
  

Here we’re asking for the inclusion of a link tag, that will link us via an AJAX request to the server. This link will trigger the list action, and will modify the DOM element named pagelist.

So now, we have to modify our list action to handle AJAX request. Although RoR gives you a way to know, in an action, if it’s called from an AJAX or a regular request, will just modify list to handle only AJAX request.

We have several solution here: either render directly HTML in the list action (violates the MVC), or use partials. Partials are template that will be inserted where ever they are called. They are often useful to render a piece of information that will appear often (for example a list of pages could appear in several pages or subpages, and using partials allows to define the appearance of this list in only 1 template).

So let’s use a partial. A partial name begins with an underscore, although this underscore doesn’t appear when we’re referring to it, and the partial template is located in the same directory as other templates. The render function allows to render partials in a very simple way, and also takes care of collections (which is quite handy in our case):

   def list
     render( :partial => "page", 
             :collection => Page.find_all,
             :layout => false            
           )
   end
 

So we’re asking for rendering of the partial _page.rhtml, using the collection Page.find_all, and asking for no layout (as we’re already rendering in a layout). Thus the partial _page.rhtml will be called for each member of the collection, and in the partial each of this member can be referred through the name page (i.e. the name of the partial).

Now let’s create app/views/wiki/_page.rhtml:

   <li><%= link_to page.name, :action => "index", :name => page.name %></li>
 

And we’re done.

One more things .. Suppose we want our list to appear smoothly ... Just change the pagelist element to:

    <ul id="pagelist" style="display: none;"></ul>
    

in order for it to be invisible at first, then add the effects javascript library:

<%= javascript_include_tag "prototype", "effects" %>

and change the link_to_remote to:

    <p>View the <%= link_to_remote "complete list of pages", 
                                   :update => "pagelist",
                                   :url => { :action => 'list'},
                                   :complete => "new Effect.Appear('pagelist')" 

                 %></p>
  

and that’s all !!! In fact the pagelist element will be updated, but being invisible you won’t see changes … until the action is completed, and the bunch of javascript (@new Effect.Appear(‘pagelist’)@) is called. It will basically display progressively the pagelist element.

An author?

OK … That’s nice, but … our pages need an author !

And all this means that our Page model should get another field: author ... and that our SQL schema is about to be modified.

To do this, we have two solutions:

As we choose to create our first schema with RoR (and as it’s a RoR tutorial and not an SQL tutorial ;), and as migrations allow to easily switch between several versions of a schema, we’re going to use migrations.

First of all, we need to note that we not only want to modify our schema, but also keep all the elements of our databse, giving the, a default value for the new column …

Let’s create a migration, and let’s name it add_author:

  script/generate migration add_author
        create  db/migrate
        create  db/migrate/001_add_author.rb
  

You can notice that our migration is now numbered (which allow to apply several migration in a row while still getting them ordered). Let’s have a look to the generated file (db/migrate/001_add_author.rb):

    class AddAuthor < ActiveRecord::Migration
      def self.up
      end

      def self.down
      end
    end
  

We basically have two methods:

In our case, we need, in up to add a column (@author) to our table (author will be a string with a default value of “AnonymousCoward”) and we decide that already existing pages will have an author named Fred>

  def self.up
    add_column :pages, :author, :string, :default => "AnonymousCoward" 
    Page.find( :all ).each {|p| p.update_attribute :author, "Fred" }
  end

Regarding down, we just need to drop the author column:

  def self.down
    remove_column :pages, :author
  end

We can now run our migration:

rake migrate

and check that our page FrontPage is now written by ‘Fred’:

    > script/console
    Loading development environment.
    >> Page.find_by_name( 'FrontPage' )
    => #<Page:0xb7473eac @attributes={"name"=>"FrontPage", "author"=>"Fred", "id"=>"2", "content"=>"Welcome to *my* __super__ front page now with WikiWord"}>
    >>
  

Et voilą :)

If we look at our schema db/schema.rb, we can see that it has not been updated. No problem. We first need to indicate to RoR that it should use Ruby has the default schema language. This can be done by editing the config/environment.rb file, in order to have/uncomment the following line:

config.active_record.schema_format = :ruby

and the by re-run our migration:

rake migrate

Looking to db/schema.rb we now have something like:

 ActiveRecord::Schema.define(:version => 1) do

   create_table "pages", :force => true do |t|
     t.column "name", :string, :limit => 30
     t.column "content", :text
     t.column "author", :string, :default => "AnonymousCoward" 
   end

 end
 

You can even notice that a version numbered just appeared in our schema definition …

Creation? When?

TODO.


Generated on Tue Sep 19 23:19:59 UTC 2006