A Look at Rails and Volume of Code

This is not a Rails tutorial. There a billion of them out there, and many of them quite good. My goal is not even to explain everything I’m doing. If you know Rails, you’ll get it, if you don’t, I refer back to my previous sentence and encourage you to at least take a look, and even if you don’t, you’ll should see that ruby is pretty easy to read. Having said that, my goal is to just take a look at a small web app I wrote, and evaluate the actual amount of code I had to write.

I want to do this because it seems that when people are doing framework comparisons, they can get a little hung up on the folder structure and volume of files. Look, with any new framework, you’re going to have to learn something new, it’s a given, but once you do, all that structure and code just sort of fades into the background, and you start concentrating on the actual work. So, we’re here to look at the actual work to create a simple application I’ve dubbed [click].

The App

It’s probably worth at least briefly describing the app, so you know what we’re trying to accomplish. The idea was for a simple photo portfolio-style site. I didn’t want a slideshow, I only wanted to display a few pictures, one at a time, with simple keyboard (or mouse) navigation. I wanted it all in HTML5, all AJAX-y (if that’s a word), but with shareable urls. Since a picture is worth a 1,000 words, a picture app must be worth 1000x, where x is the number of pictures on the site. (Sorry, math humor.) Anyway the point is: it’s up, so you can see the final product here.

Setup

On to the meat. Let’s make an app. First, we’re going to need Rails. For our purposes all commands are from the root folder that was created for this project. Assume your environment is already setup (again, part of learning the framework, not using it.), and all lines starting with $ are bash/shell.

$ rails new click

Files and folders created. Now we’re going to need a few libraries to accomplish what we want, so we edit /Gemfile to be:

source 'http://rubygems.org'

gem 'rails', '3.0.7'
gem 'sqlite3-ruby', '1.3.2', :require => 'sqlite3'
gem "paperclip", "~> 2.3"
gem 'typus'
gem 'exifr'

group :development do
  gem 'rspec-rails', '2.5.0'
  gem 'annotate-models', '1.0.4'
end

group :test do
  gem 'rspec', '2.5.0'
  gem 'webrat', '0.7.1'
end

Typus will generate our admin for us, paperclip performs attachment magic, exifr reads EXIF data, and sitemap_generator does what it’s name suggests. The rest are for testing/convenience. Now we:

$ bundle install

And all our libraries are ready.

The Model

We need to store our images and some info, so let’s create a model.

$ rails generate model photo name:string taken_on:date exposure:string f_stop:string focal_length:string iso:string description:string
$ rails generate paperclip photo image

Now, for our functionality. The model is the meat of this application, and thus follows /app/models/photo.rb:

class Photo < ActiveRecord::Base
  has_attached_file :image, :styles => { :display => "1050x700>", :rss => "400x300>" }

  validates :name, :presence => true
  validates :description, :presence => true

  validates_attachment_presence :image
  validates_attachment_content_type :image, :content_type => ['image/jpeg']

  scope :sorted, order('taken_on DESC')

  before_save :load_exif

  def load_exif
    # Pull the Exif data
    exif_data = EXIFR::JPEG.new(self.image.to_file)
    return if exif_data.nil? or not exif_data.exif?
    # Set the attributes
    self.exposure = exif_data.exposure_time.to_s
    self.f_stop = exif_data.f_number.to_f.to_s
    self.focal_length = exif_data.focal_length.to_f.round.to_s
    self.iso = exif_data.iso_speed_ratings
    self.taken_on = exif_data.date_time.to_date
  rescue
    false
  end

  def info_line
    "#{self.focal_length}mm #{self.f_stop}f #{self.exposure} ISO #{self.iso}"
  end

  def self.id_list
    @id_list = Array.new
    self.sorted.each { |photo| @id_list << photo.id }
    @id_list
  end

  def next_id
    @id_list = Photo.id_list
    @id_list.last.id == self.id ? @id_list.first : @id_list[@id_list.index(self.id) + 1]
  end

  def prev_id
    @id_list = Photo.id_list
    @id_list.first.id == self.id ? @id_list.last : @id_list[@id_list.index(self.id) - 1]
  end

end

What’s that do? It adds file attachment support to the photo object, and automatically creates two more versions of the file at the specified sizes. It makes name, description, and an image that is a jpeg mandatory. Before saving the record, it pull the EXIF data from the image file. Finally, there are some helpers for display and ordering.

Done. Let’s create the DB.

$ rake db:migrate

We now have the database created, table for our model created, some helpers for display and all our image attachment handling taking care of.

The Controller

Time to power up the retrieval of this data. For that we’ll need a controller.

$ rails generate controller Photos

And we’ll need to respond to json requests only (we’re all AJAX).

class PhotosController < ApplicationController
  responds_to :json

  # GET /photos/1.json
  def show
    @photo = Photo.find(params[:id])
    @photo_json = {
      'title' => @photo.name,
      'description' => @photo.description,
      'taken' => @photo.taken_on.strftime('%b %d, %Y'),
      'info' => @photo.info_line,
      'url' => @photo.image.url(:display),
      'next_id' => @photo.next_id,
      'prev_id' => @photo.prev_id
    }
    respond_with @photo_json
  end

  # GET /photos/first.json
  def first
    params[:id] = Photo.id_list.first
    show()
  end

end

Now when you call /photos/X.json, you’ll get JSON encoded information about the photo (id=X) to display.

The Admin

Typus will handle our admin interface. It will provide a user system, understands ActiveRecord models and handles paperclip attachments, so we just need a few commands.

$ rails generate typus
$ rails generate typus:migration
$ rails generate typus:controller photo
$ rake db:migrate

Here’s what that looks like:

.

We’re Done!

Well, more specifically, we’re done the Rails part. We will ultimately add a static html homepage, css styles, and an application javascript file that powers the display. This page and javascript has nothing to do with Rails, and would be the same regardless of framework, so it’s not really part of this discussion. Things I didn’t do include write any SQL, extend the framework at all, handle image uploads and/or resizing, put one line of code into a backend, manage any JSON encoding, manually manage any revisions to the DB during development (migrations are fantastic!), or set up a development and test environment or server (done automatically).

Disclaimer/Conclusion

I’m not Rails expert. In fact, I’m barely qualified to be called a beginner. There are things above that could be done better, there are even more things like caching and minification that Rails could do for me with a few lines of code. There’s a whole testing component to the app that was 75% auto-generated by Rails that I didn’t even mention. To scale this app up, you’d want to run the database on a database server. If you’re really interested, you can get the full app from my github repo. Having said that, I don’t feel that any of that matters for this example. What matters is that having been working programmer in Java/PHP/Perl/ColdFusion for over a decade, I can see why so many of the frameworks in those languages are basically taking some or all of their feature set from Rails. I enjoyed working on this codebase, and hope to continue improving it as I have free time or think of new ideas because it doesn’t require jumping through hoops. I’m not about to start pushing hard to switch everything to Rails, because I tend to be a bit of a language agnostic, and I think there’s a right tool for every job. However, I have a feeling that Rails IS the right tool for many MVC web projects, and definitely think it warrants a deeper look if you haven’t had the opportunity to do so.

This entry was posted in Lesson and tagged , , , , . Bookmark the permalink. Follow any comments here with the RSS feed for this post.

University of Pennsylvania Logo
Copyright © 2012 The Wharton School, University of Pennsylvania