Testimony Part V: What’s in a Name?

Last time, I promised that we’d make our testimonial application more friendly for users and less so for spammers and that’s what we’re going to do today, among other things of course. Let’s get’r done!

Somehow, we overlooked the user’s name when we created the schema for the testimonial leaving us only the email address to use as an attribution. Fortunately Rails’ migrations makes it trivial to add the new field. Drop into the shell and generate a migration:

script/generate migration AddUserName
     create  db/migrate
     create  db/migrate/004_add_user_name.rb
end

Open that in your editor, and make it look like the following. I think 64 characters is long enough to hold a name for this app.

class AddUserName < ActiveRecord::Migration
  def self.up
    add_column :testimonials, :name, :string, :limit => 64
  end  
	
  def self.down
    remove_column :testimonials, :name
  end
end

Alright, now we’ve added a place to store the user’s name.; we’ll let ActiveRecord handle the CRUD for us but it’s still our task to ask the user to enter their name. To that end, we need to edit the form that is used to edit a testimonial. We factored that out of the add view into a partial named _form.rhtml. Let’s ask for our customer’s name before we ask them to give up their email address, that seems more neighborly, don’t you think? At the top of the form, add the following:


<p><label for=\"testimonial_name\">Your name</label><br/>
<%= text_field 'testimonial', 'name'  %></p>

text_field is part of ActionView’s FormHelper module. It creates an input field of type text and wires it up to display and update the object and field specified by the first and second parameters, respectively. In this case, it will initially fill the text input with the value returned by Testimonial.name. Later, when the form is posted, it will take the text that is in the input field and store it in the testimonial object, using the same method.

While we’re in the form, it bugs me that the scaffolding code didn’t really provide a good implementation for the approved field; it always defaulted to false, regardless of the value in the object. I fixed it as follows:

<option value=\"false\" <%= 'selected=\"yes\"' unless @testimonial.approved %>>False</option>
<option value=\"true\" <%= 'selected=\"yes\"' unless !@testimonial.approved %>>True</option></select></p>

In a later installment, we’ll get rid of the select in favor of the more natural UI element for representing a boolean value: a checkbox ( you’d think that the generator would have done that for us, but noooooo).

Note that to do all that, I did not have to make a single change to the TestimonialController class — that’s the magic of Rails. There was a slight change to the file, however, since I use and highly recommend Dave Thomas’s (of Pragmatic Programmer fame) annotate_models plugin. It’s a magical thing..whenever you update your schema via a migration, your controller files are updated to show the current schema at the top, like so:

# == Schema Information
# Schema version: 4
#
# Table name: testimonials
#
#  id         :integer(11)   not null, primary key
#  email      :string(255)
#  testimony  :text
#  approved   :boolean(1)
#  created_on :date
#  updated_on :date
#  name       :string(64)
#
	
class Testimonial < ActiveRecord::Base
	
  # only validate the two user-supplied fields
  validates_presence_of :email, :testimony
	
  # make sure the testimony is a reasonable length
  validates_length_of  :testimony, :within => 10..4096
  # make sure the email address looks usable
  validates_format_of       :email, :with => /(^([^@\s]+)@((?:[-_a-z0-9]+\.)+[a-z]{2,})$)|(^$)/i
	
end

I love it, it beats floundering around switching from schema.rb to your source files.

Remember the tools div we added last time? Well, let’s make it a little better. After all, we’re being agile and making continuous improvements. We have links to edit and delete actions , if the user is logged in so why not allow instant approval as well? Here’s the code for the all action (all.rhtml)

<h2>Look what our customers are saying!</h2>
	
<% for testimonial in @testimonials %>
<% if testimonial.approved? || logged_in? -%>
  <div class=\"testimonial\">
    <h5>On <%=h testimonial.created_on.strftime(\"%B %d %Y\") -%>, <%=h testimonial.name || testimonial.email -%> said:<%= image_tag(\"comment.png\", :size =>\"16x16\", :border => 0)%></h5>
    <p><%=h testimonial.testimony %></p>
    <% if logged_in? -%>
    <div class=\"tools\">
      <ul>
          <li><%= link_to image_tag(\"comment_edit.png\", :size =>\"16x16\", :border => 0) + \"Edit\", { :action => 'edit', :id => testimonial} %></li>
          <li><%= link_to image_tag(\"comment_delete.png\", :size =>\"16x16\", :border => 0) + \"Delete\", { :action => 'destroy', :id => testimonial }, { :confirm => 'Are you sure?', :method => :post }%></li>
          <% if !testimonial.approved? %>
          <li><%= link_to 'Approve', :action => 'approve', :id => testimonial %></li>
          <% end %>
      </ul>
    </div>
    <% end %>
  </div>
  <% end %>
<% end %>
	
<%= link_to 'Previous page', { :page => @testimonial_pages.current.previous } if @testimonial_pages.current.previous %>
<%= link_to 'Next page', { :page => @testimonial_pages.current.next } if @testimonial_pages.current.next %> 
	
<br />
	
<%= link_to image_tag(\"comment_add.png\", :size =>\"16x16\", :border => 0) + 'Add your testimonial', :action => 'new' %>

Aside from adding icons to each link (made easy with image_tag), we now check to see if the user is logged in (line 4), and if so, we show the testimonial regardless if it’s been approved or not. At line 13, if the testimonial hasn’t been approved, we show a link to the user that will approve the testimonial. I wonder if there’s a more concise way of writing lines 13-15… a link_to unless logged_in? would work, except that we’d end up with an empty list item; if you know of one, please let me know. This change is the first one that impacts our controller, we need to write the approve method in TestimonialController:

def approve
  @testimonial = Testimonial.find(params[:id])
  @testimonial.update_attributes(:approved => true)
  redirect_to :action => 'all'
end

First, we find and load the testimonial that we were editing, specified by the id parameter baked into the form and use the Rails method update_attributes to set approved to true. Note that update_attributes saves the model object for us. We send the user on their merry way, back to looking at all the testimonials.

While I was in the controller, I changed the list action to fetch the testimonials ordered by date submitted. That was as easy as adding :o rder =>"created_on DESC"> at the end of the call to paginate. You know, that’s one of the reasons I like rails than any C++ framework I’ve ever used… it enables you to concentrate on the task at hand, rather than language scaffolding. I’ve been using C++ for many years now and have sung it’s praises to many but ruby and rails is taking the fun out of C++ for me. That’s a story for another day though.The last change I wanted to get to in this installment is a small tweak to the submission process. After a user submits a testimonial, I want to display a “thank you” page that explains the submission process a little bit. This is a pretty trivial task, all we need to do is change Testimonial.create as follows:

  def create
    @testimonial = Testimonial.new(params[:testimonial])
    if @testimonial.save
      flash[:notice] = 'Testimonial was successfully created.'
      redirect_to :action => 'thanks'
    else
      render :action => 'new'
    end
  end

Did you spot the change? Yup, right there on line 5 we redirect the user to the ‘thanks’ action which means we need a new view. Since there’s no code behind the action, we don’t need a method in the controller… just the view:

<h2>Thanks!</h2>
<p>Thank you for your comments. Your testimonial will be reviewed soon. Because of the number of submissions we receive, not all testimonials will be shown at the same time.</p>
<p><%= link_to 'Go back', :action => 'all' %> and read more testimonials from other customers.</p>

Pretty straightforward, like much of the easy parts of Rails. Tell the user that their submission might not make it to the testimonial page, give them a reason why, and give them a link back to all of the testimonials.

Well, that wraps it up for this installment. As always, you can play with the running application at the demo site. To roleplay as Julie, our site admin, login as user demo; the password is demo.

Thanks for reading!

Comments are closed.