Quantcast
Channel: Building Web Apps
Viewing all articles
Browse latest Browse all 27

Lab 8 (Bonus): A Page Model and Cleanup

$
0
0

This final RailsQuickStart lab write-up describes changes we’ve made to the sample application. Some changes are just code clean-up, while others implement some of the additional features suggested at the end of each lab.

To use the new code, check it out from our shared Subversion repository (see the Lab 7 writeup if you need a refresher on how to do this) and migrate to the current database version (rake db:migrate).

Page model

The most dramatic change is the addition of a Page model, and its associated pages_controller and views. This replaces the old static_controller, which we’ve purged. By using a model to store the content and metadata for each page in the database, an administrator can update page titles and set page description and keyword metadata without having to edit code.

Controller and view code

The page model is administered just like any other, with the usual create and edit views.

To render each page, we added an action called simply “view”. The method in pages_controller is:

  def view
    unless @page = Page.find_by_name(params[:name])
      render :action => 'error' and return
    else     
      login_required if @page.protect
      @current_tab = @page.navtab
    end
  end

We find the page by its name, and if there is no page of a matching name, we render the error page. Assuming it is found, we check its ‘protect’ attribute, and if that is set, we require login to view the page. Finally, we set the @current_tab value, which is used by application layout to highlight the appropriate tab.

The “view” template is very simple, since all it is doing is rendering the contents of the page body attribute:

<%= render_to_string :inline => @page.body %>

Note that we are not just using <%= @page.body %> here, because we want the body to be intepreted as ERb. Because the render_to_string method is not normally available in views, we need to add this line to pages_controller to make it available as a view helper:

	helper_method :render_to_string

The joys and dangers of ERb

The body for each page, as read from the database, is interpreted as ERb, so you can use helpers and other code. This gives you complete control over what goes on each page; you can use the helpers we wrote for content blocks and images to include items from those database tables, and you can use the usual ERb code to display data from other models.

There is a danger to this, however, which you should be aware of: there is no restriction on what code can go in an ERb block, so it could, for example, delete everything from the database. It can even issue system commands and wipe the file system. With great power comes great responsibility. Presumably, the only people who would be allowed to log in and edit the pages would be people you could trust.

If you want to use a similar approach but in a safer way, take a look at the Liquid templating language. It is similar to ERb for displaying data but does not allow destructive operations or access to system commands.

Creating pages

In addition to the migration that creates the Page model, we added a migration that populates the three pages that used to be handled by the static controller. You can edit the content of these pages by going to the main admin page, clicking on Pages Admin, and then clicking the Edit link for the appropriate page.

If you add new pages, you’ll need to add them to the navigation bar, or put a link on another page somewhere, so users can access them. You could take this design a step further by creating the nav bar from the Page model, so that each new page would automatically get a nav button.

Note that pages that are created from other models, such as the resources page, are independent of the Page model, and for those pages, the page title and metadata are set via instance variables in the controller, just as we did in the static controller. We’ve made changes to the application layout so the description and keyword metadata can be set, in addition to the page title.

Putting metadata in the layout

We want to take the metadata (title, description, and keywords) for each page and put it in the page header, so we need to do that in the layout. These lines go in the head section of views/layouts/application.html.erb:

<% if @page %>
      <title><%= @page.title %></title> 
      <meta name="keywords" content="<%= @page.keywords %>" />
      <meta name="description" content="<%= @page.description %>" />
   <% end %>

If there is a page object (remember that some pages, like the resources page, are not generated from page objects), then we render the HTML required for each piece of metadata, pulling the contents from the appropriate attribute of the @page object.

Supporting routes

We added a couple of routes to support the Page model:

   map.root :controller => "pages", :action => "view", :name => 'home'
   map.connect ':name', :controller => "pages", :action => 'view'

The first route makes the page named “home” function as the home page of the site.

The second route causes any URL that consists of a single word to invoke the view action in the pages controller, and pass that word as the :name parameter.

Resource page enhancements

We modified the view for the resources page so it doesn’t show a category heading if there are no links for that category. This is just a one-line change in links/resources_page.html.erb:

<% if category.links.size > 0 %><h2><%= category.name %></h2><% end %>

We also added a route for the resources page so the simple URL“/resources” would work:

   map.resources_page '/resources', :controller => 'links', :action => 'resources_page'

Since we gave the route a name (after “map.”), we can refer to it in other places, such as in the nav link in the application layout, as resources_page_path.

User management

We added an admin page that lists all the users, and allows you to delete users and to change their passwords. Take a look at users_controller and views/users/index.html.erb to see how this works. Here’s the heart of the index view:

<table>
<% @users.each do |user| %>
   <tr>
      <td><%= user.login %></td>
      <td><%= user.email %></td>
      <td><%= link_to "Delete", user, :confirm => 'Are you sure?', :method => :delete %></td>
      <td><%= link_to "Change Password", :controller => 'users', :action => 'change_password', :id => user %></td>
   </tr>
<% end %>
</table>

On each row of the table, we show one user’s login and email, and provide links to delete that user or change their password.

Creating the first user

We had disabled the requirement for login to access the users controller so you could create your first user, but obviously this makes the entire system completely insecure. So we’ve re-enabled the login requirement.

This creates a quandary if you’re starting with an empty database: you can’t log in to create your first user if there are no users. You could use the Rails console to create a user, but to make this easier, we added a migration that creates a user “admin” with the password “seminar” (in the file db/migrate/013_create_admin_user.rb):

   def self.up
      User.create(:login => 'admin', 
                  :email => 'noone@anydomain.com', 
                  :password => 'seminar', 
                  :password_confirmation => 'seminar')
   end

You should either change the password (which you can now do via the Users Admin page) or delete this user if you’re using this code in a production environment.

Making things a little prettier

We’ve added some styling to the forms, so they’re a little more respectable. This includes changes in the html pages, as well as some additional CSS styles. We’ve also linked the standard Rails scaffold.css file in the application layout, which provides styling for validation error messages and highlights fields with errors.

There’s one thing we don’t like about the way the standard Rails error display works. Fields that have errors are wrapped in a div that has an ID of “fieldWithErrors”. This lets the stylesheet apply different styling to fields that have errors.

But a div is the wrong element to use for this; for one thing, it messes up the layout of the forms when there is an error. We added a little code to environment.rb that makes Rails use a span, instead of a div, to avoid this problem:

   ActionView::Base.field_error_proc = Proc.new{ |html_tag, instance| "<span class=\"fieldWithErrors\">#{html_tag}</span>" }

This is a little obscure, but we didn’t have to figure it out ourselves; a little bit of Google searching with a description of the problem revealed this solution.

On the new subscription page, we combined all the error messages into one block by listing all of the models on one line:

<%= error_messages_for :subscription, :customer, :creditcard %>

Finally, we added some styles to our quickstart.css style sheet that override some of the styles in the standard Rails scaffold.css. We also added a little default styling for the table element so the admin pages would look a little better.

Testing Purchases Through Mock ActiveMerchant Gateway

In the code we developed previously, we weren’t testing the creation of a subscription (where ActiveMerchant gets called). In this bonus lab, we refactored SubscriptionsController.create to lend itself to testing. We needed to make a few changes:

  1. change the create action itself to allow swapping in a test gateway object;
  2. change the environment setup files to use the proper gateway depending on whether you were in test, development, or production;
  3. and add some tests.

Fortunately, ActiveMerchant provides many tools to help with testing. The PeepCode Active Merchant PDF is an excellent resource that you should check out.

Refactored SubscriptionsController.create

We refactored the code to look like this:

def create
  # render :inline => "<%= debug params %>" and return
  @current_tab = "subscribe_tab"
  @page_title = "Please correct the errors below"
  @subscription = Subscription.new(params[:subscription])
  @subscription.amount = Subscription.cost_for_period(@subscription.period)
  @customer = Customer.new(params[:customer])
  # create the creditcard object and get name from customer object
  @creditcard = ActiveMerchant::Billing::CreditCard.new(params[:creditcard])    
  @creditcard.first_name = @customer.first_name
  @creditcard.last_name = @customer.last_name
  # make sure all three models are valid
  @customer.valid?
  @subscription.valid?    
  #
  # Our test code uses bogus credit cards for signaling various conditions to the 
  # bogus gateway. In test mode, the creditcard is never "valid", so work around that
  # here. The better thing to do if you are going to do creditcard stuff in the future
  # is to refactor this logic into separate classes. See PeepCode's ActiveMerchant PDF
  #
  unless (ENV['RAILS_ENV'] == 'test' or @creditcard.valid?) and @customer.errors.empty? and @subscription.errors.empty?
    render :action => :new and return
  end
  # if everything looks ok, send the purchase request to the gateway
  # set up options hash with billing address and ip
  options = {
    :ip => request.remote_ip,
    :billing_address => { 
      :name     => @customer.full_name,
      :address1 => @customer.address,
      :address2 => '',
      :city     => @customer.city,
      :state    => @customer.state,
      :country  => 'US',
      :zip      => @customer.zip,
      :phone    => @customer.phone
    }
  }  
  begin
    response = Subscription.gateway.purchase(@subscription.amount, @creditcard, options)
    # redisplay form if not successful
    unless response.success?
      flash[:notice] = "Transaction failed: #{response.message}"
      render :action => :new
    else
      # save the responses from the transaction in the subscription record
      @subscription.message = response.message
      @subscription.reference = response.authorization
      @subscription.test = response.test?
      @subscription.params = response.params.to_yaml
      @subscription.customer = @customer
      @subscription.save!
      flash[:notice] = "Thanks for subscribing to our site!"
      redirect_to root_path
    end
  rescue ActiveMerchant::ActiveMerchantError => e
    # Consider much better error handling (of ActiveMerchant specific exceptions)
    flash[:notice] = "Transaction failed: #{e}"
    render :action => :new
  end        
end

The unless clause that tests for valid objects changed. Our tests will use a bogus credit card object that ActiveMerchant’s test code knows how to use. The fake credit card encodes whether the mocked merchant gateway should succeed or not. This means we can’t use the validation check when we are in test mode.

We also rewrote the gateway usage itself. Now, we use a gateway that is stored in a class variable of Subscription. The gateway is created at initialization time, and this allows us to swap in different gateways if desired. ActiveMerchant supplies a mocked gateway and we use that when testing.

Note we also put the gateway usage between exception handling code (the begin/rescue/end blocks). If something goes wrong in the gateway during testing, or during real use, we catch that problem and send the user back to the data entry form.

Environment File Change and Subscription Model Tweak

Since we are creating the appropriate gateway at startup, we need to do that in our environment files. The code for test.rb looks like:

config.after_initialize do
  ActiveMerchant::Billing::Base.mode = :test
  Subscription.gateway =
    ActiveMerchant::Billing::BogusGateway.new
end

We are simply putting ActiveMerchant into test mode and setting the class variable in Subscription to hold an instance of the mock gateway object called ActiveMerchant::Billing::BogusGateway.

The Subscription class needed a small change to add the variable:

class Subscription < ActiveRecord::Base

  belongs_to :customer  
  validates_numericality_of :amount, :message => "Choose a subscription amount"
  validates_numericality_of :period, :message => "Choose a subscription amount"
  
  # The payment gateway we want to use is set in our environments, so we can
  # swap it out for testing
  cattr_accessor :gateway
  
  def self.cost_for_period period
    if period == 1
      995     # $9.95 in cents
    elsif period == 12
      9995    # $99.95 in cents
    else
      "invalid"
    end
  end
end

cattr_accessor is a method that creates a class level set of data accessors for a variable, in this case called gateway.

Some Tests

We tweaked the test_helper.rb file to add a helper that makes a creditcard hash object for us:

def credit_card(options = {})
  { :number => '1',
    :first_name => 'Bob',
    :last_name => 'Sample',
    :month => '8',
    :year => "#{ Time.now.year + 1 }",
    :verification_value => '123',
    :type => 'visa'
  }.update(options)
end

and we use it in several functional tests in our subscriptions_controller_test.rb file:

def test_should_create_subscription
  assert_difference('Subscription.count') do
    post :create, :subscription => { :amount => 1, :period => 1 }, 
                  :customer => {:first_name => 'bob', :last_name => 'Sample', :address => '123 main', :city => 'San Jose', :state => 'CA', :zip => '95000'},
                  :creditcard => credit_card({:number => '1'})
  end

  assert_redirected_to root_path
end

def test_should_not_create_subscription_failed_auth
  assert_no_difference('Subscription.count') do
    post :create, :subscription => { :amount => 1, :period => 1 }, 
                  :customer => {:first_name => 'bob', :last_name => 'Sample', :address => '123 main', :city => 'San Jose', :state => 'CA', :zip => '95000'},
                  :creditcard => credit_card({:number => '2'})
  end
  
  assert_response :success
end

def test_should_not_create_subscription_activemerchant_exception
  assert_no_difference('Subscription.count') do
    post :create, :subscription => { :amount => 1, :period => 1 }, 
                  :customer => {:first_name => 'bob', :last_name => 'Sample', :address => '123 main', :city => 'San Jose', :state => 'CA', :zip => '95000'},
                  :creditcard => credit_card({:number => '3'})
  end
  
  assert_response :success
end

The only difference between these three tests is setting the number of the creditcard to 1, 2, or 3. These magic numbers are used by the mock object to signal three cases: 1) good validation; 2) bad credit card validation; and 3) a fault in the gateway. See the ActiveMerchant documentation for more info.

But wait, there’s more!

There’s an assortment of other changes throughout the code: adding validations for most model fields, making the defensio code a little more robust, and fixing some tests.

What’s next?

That’s all, folks. If you find any issues with the code, or want to make any suggestions, please post to the Google group.


Viewing all articles
Browse latest Browse all 27

Trending Articles