3.2. Documents

has_many and belongs_to allow to have 1 to n relations betweens objects. But here we want to use n to n relations : one document can be in many categories and categories can have many documents. In fact it’s two 1 to n relations. So we are going to use the has_and_belongs_to_many and belongs_to attributes.

Open the app/models/document.rb file. This is where the Document class is defined. Add the following two lines in the class :

  has_and_belongs_to_many :categories
  belongs_to :author

We have defined a relation with two other models : categories and author. So : the document belongs and has many categories, and he belongs to one author only. We know will be able to access to the categories and author attributes. The categories attribute will be a collection, author a simple object. Now we are going to define the methods of the document controller and then the views. Here is the controller’s methods :

model :document

def index

end

def    list
    @publications = Document.find_all()            # get all the documents
end

def show
    @item = Document.find(@params["id"])        # get one document using the id to
                                                            # know which one is the good one
end    

def new
    @categories = Category.find_all                # get all the categories
    @year = Time.now.year                            # get the year
    @month = Time.now.month                            # get the month
    @monthname = Time.now.strftime("%B")        # get the month name
    @day = Time.now.day                                # get the day
end

def create 
    @categories = Category.find_all                # get all the categories
    item = Document.new  # Create a new instance of Todo, so create a new item
    item.attributes = @params["document"]  # The fields of item should be set to what's in the "new_item" hash

    if item.save  # Try to save our item into the database
        for category in @categories                # if it is saved we can add the categories document links
            if (@params[category.name])            # so we look in the params if there is one who got the same
                                                            # name as the category
                item.categories<<(category)        # if there is one it is because the user has checked the box
                                                            # so we insert the category in the collection using the '<<' method
            end
        end
        redirect_to(:action => "list")  # Return to the list page if it suceeds
    else
        render_text "Couldn't add new item"  # Print an error message otherwise
    end
end

# get all the things we need to display a real edit form
def edit
    @categories = Category.find_all                # get all the categories
    @year = Time.now.year                            # get the year
    @month = Time.now.month                            # get the month
    @monthname = Time.now.strftime("%B")        # get the month name
    @day = Time.now.day                                # get the day
    @document = Document.find(@params["id"])    # get the good document
end

# update method, called by the edit form
def update
    @categories = Category.find_all                                # get all the categories
    @document = Document.find(@params['document']['id'])    # get the document into the db using the id passed in the
                                                                            # form to get the good one
    if @document.author_id == @session["author_id"]            # verify if the logged user has the write to edit the document
        @document.attributes = @params['document']            # update the attributes
         if @document.save                                                # try to save the document
            @document.categories.clear                                # if it is saved we clear all the links
                                                                            # relative to this document in the categories_document table
            for category in @categories                            # we recreate the good links
                if (@params[category.name])
                    @document.categories<<(category)
                end
            end
           flash['notice'] = 'document was successfully updated.'
           redirect_to(:action => 'show', :id => @document.id) # it's ok so we redirect
         else
           render_action 'edit'                                            # not ok go back to the edit page
         end
    else
        flash['notice'] = 'You can\'t edit this article.'        # the logged user doesn't have the right to edit the doc
        redirect_to(:action => 'list')
    end

end

# define the secure methods
protected
def secure?
  ["new", "edit", "update", "create" ].include?(action_name)
end

Now we will create the views :
  • list.rhtml
  • show.rhtml
  • new.rhtml
  • edit.rhtml
list.rhtml :

    <h1>Documents</h1>
    <%= link_to("New document", :controller => "document", :action => "new") %>
    <ul>
    <% @publications.each do |@item| %>
        <li>
          <%= link_to(@item.title, :controller => "document", :action => "show", :id => @item.id) %> @
      <% for @cat in @item.categories %> 
      <%= link_to(@cat.name, :controller => "category", :action => "show", :id => @cat.id) %>, 
      <% end %>
      <% if (@session["author_id"] && @session["author_id"] == @item.author_id ) %>
        :: <%= link_to("Edit", :controller => "document", :action => "edit", :id => @item.id) %>
        <% end %>
      </li>
          <br />
     <% end %>
    </ul>

Ok same idea as previous list view. For each publication/document we display the title and the categories where it is. We display an edit link if the logged user is the document’s author. show.rhtml :

   <h1><%= @item.title %></h1>
   <p>
    <%= textilize_without_paragraph(@item.description) %>
   </p>
   <p>File : <a href="/pub/<%= @item.filename %>"><%= @item.filename %></a><br />
   In :
    <% for @cat in @item.categories %> 
      <%= link_to(@cat.name, :controller => "category", :action => "show", :id => @cat.id) %>, 
    <% end %><br />
</p>

Simple : display the document’s info. This view introduce a redcloth method : textilize_without_paragraph(). It transform a textile formated text into html. You must have RedCloth installed to be able to use it. new.rhtml

    <h1>New Document</h1>

    <form action="/document/create" method="post">
    <p><label for="document_title">Title</label><br />
    <input id="document_title" name="document[title]" size="30" type="text" value="" /></p>
    <p><label for="document_description">Description</label><br />
    <textarea cols="65" id="document_description" name="document[description]" rows="15" wrap="virtual">
    </textarea></p>
    <p><label for="document_date">Date</label><br />
    <select name="document[date(1i)]">
    <% for @ynum in (@year-2..@year+10) %>
      <% if @ynum == @year %>
        <option value="<%= @ynum %>" selected="selected">
      <% else %>
        <option value="<%= @ynum %>">
      <% end %>
      <%= @ynum %></option>
    <% end %>
    </select>        
    <select name="document[date(2i)]">
    <% for @mnum in (1..12) %>
      <% if @mnum == @month %>
        <option value="<%= @mnum %>" selected="selected">
      <% else %>
        <option value="<%= @mnum %>">
      <% end %>
      <%= @mnum %></option>
    <% end %>
    </select>

    <select name="document[date(3i)]">
    <% for @dnum in (1..31) %>
      <% if @dnum == @day %>
       <option value="<%= @dnum %>" selected="selected">
      <% else %>
       <option value="<%= @dnum %>">
      <% end %>
      <%= @dnum %></option>
        <% end %>            
    </select>
    </p>
    <p><label for="document_categories">Categories</label><br />
        <% for @category in @categories %>
      <%= @category.name %> : <input type="checkbox" name="<%= @category.name %>" value="0"/><br />
    <% end %>
    </p>
    <input type="hidden" name="document[author_id]" value="<%= @session["author_id"] %>"/>
    <p>
    <label for="document_filename">Filename</label><br />
    <input id="document_filename" name="document[filename]" size="30" type="text" value="" /></p>
    <input type="submit" value="Create" /></form>

Big … But quite simple in fact. You just have to look to the date select list, this is where the most difficult thing is. Remember the new method of the document controller. There was some variables to store the day, the month and the year. We use them here to get the good date selected in the select lists.

edit.rhtml :

    <h1>Edit Document</h1>

    <%= form_tag(:action => "update")  %>
    <p><label for="document_title">Title</label><br />
    <input id="document_title" name="document[title]" size="30" type="text" value="<%= @document.title %>" />
    </p>
    <p><label for="document_description">Description</label><br />
    <textarea cols="65" id="document_description" name="document[description]" rows="15" wrap="virtual">
    <%= @document.description %>
    </textarea></p>
    <p><label for="document_date">Date</label><br />
    <select name="document[date(1i)]">
    <% for @ynum in (@year-2..@year+10) %>
      <% if @ynum == @year %>
        <option value="<%= @ynum %>" selected="selected">
      <% else %>
        <option value="<%= @ynum %>">
      <% end %>
      <%= @ynum %></option>
    <% end %>
    </select>

    <select name="document[date(2i)]">
    <% for @mnum in (1..12) %>
      <% if @mnum == @month %>
        <option value="<%= @mnum %>" selected="selected">
      <% else %>
        <option value="<%= @mnum %>">
      <% end %>
    <%= @mnum %></option>
    <% end %>
    </select>

    <select name="document[date(3i)]">
    <% for @dnum in (1..31) %>
      <% if @dnum == @day %>
        <option value="<%= @dnum %>" selected="selected">
      <% else %>
        <option value="<%= @dnum %>">
      <% end %>
        <%= @dnum %></option>
    <% end %>            
    </select>
    </p>
    <p><label for="document_categories">Categories</label><br />
    Exist in : 
    <% for cat in @document.categories %>
      <%= cat.name %>, 
    <% end %>
    <br  />
    <% for @category in @categories %>
      <% if @document.categories.include? @category %>
        <%= @category.name %> : <input type="checkbox" name="<%= @category.name %>" value="0" checked="checked"/><br />
      <% else %>
        <%= @category.name %> : <input type="checkbox" name="<%= @category.name %>" value="0"/><br />
      <% end %>
    <% end %>
    </p>
    <input type="hidden" name="document[author_id]" value="<%= @session["author_id"] %>"/>
    <input type="hidden" name="document[id]" value="<%= @params["id"] %>"/>
    <p><label for="document_filename">Filename</label>
    <br />
    <input id="document_filename" name="document[filename]" size="30" type="text" value="" /></p>
    <input type="submit" value="Update" />
    <%= end_form_tag %>

Quite the same one, but look, we add the value of the document in the form this time.

Open the http://howto/document/list page. You should see nothing appart the title and the new article link. Click on it. Here is the new article form (if you are logged in) !. And you can see that there is one checkbox for each category you’ve previously created. Complete the form, check one or more category and click on the create button. And voila here you are !