read rails warnings!

Posted by acts_as_flinn Mon, 23 Jul 2007 15:57:00 GMT

I was dealing with an issue today were I was certain my code was right. I was getting the dreaded ActionController::DoubleRenderError error. I was sure I could just redirect_to ... and return but I failed to read the rails warning for the last day or so. The following comes literally right from the development log I’ve been staring at.

ActionController::DoubleRenderError (Render and/or redirect were called multiple times in this action. Please note that you m
ay only call render OR redirect, and only once per action. Also note that neither redirect nor render terminate execution of 
the action, so if you want to exit an action after redirecting, you need to do something like "redirect_to(...) and return". 
Finally, note that to cause a before filter to halt execution of the rest of the filter chain, the filter must return false, 
explicitly, so "render(...) and return false".)

I had a professor in college who used to tell me to read every sentence in a newspaper article because the whole story comes out in the last sentence. There it was plain as day, to halt the filter chain I need to return false, dang!

Rails: Separating Asset Folders by Module

Posted by acts_as_flinn Tue, 24 Apr 2007 23:37:00 GMT

Like a lot of people writing apps with a public frontend and a private admin section I like to separate public and private controllers. This makes it easy to do a number of things, like authentication and keeping code clean. In order to do this, you can use or generate modules like so:

$ ./script/generate scaffold admin::frobnicator

This will generate (among other things)

  • app/controllers/admin/frobnicators_controller.rb
  • app/views/admin/frobnicators/

It becomes a pain when your frontend images and admin images start to intermingle. One approach for example is to put assets into their own subfolders like so

  • public/images/admin
  • public/javascripts/admin
  • public/stylesheets/admin

This works to keep your asset folders from looking like a clusterfuck getting to mixed up.

The Problem

The problem really comes when you start mucking around in views. You’ve now got to prefix every resource like so:

stylesheet_link_tag(‘admin/frobnicators’)
image_tag(‘admin/frobnicators.png’)

Then do this in your stylesheets like so:

  background: url(/images/admin/frobnicators-background.png);

Ok, so here’s it’s not so bad, do this with a half a dozen controllers and the views (and every link_to) and stylesheets that come along and it starts to become a pain in the ass to prefix everything with admin.

Solution

Fortunately there is a simple solution, overflow compute_public_path. By overflowing compute_pubic_path you can create folders for modules, then reference them as usual.

In my module controllers, I will usually extend them like so:

class Admin::FrobnicatorsController < AdminController

This allows me to do fun stuff like specify an admin layout for all the admin controllers

class AdminController < ApplicationController
  layout ‘admin’
...

This has the nice side effect of looking for the admin helper ( helpers/admin_helper.rb ) with all of the module controllers. You can now overflow compute_public_path to your hearts content.

module AdminHelper
  def compute_public_path(source, dir, ext)
    dir = "admin/#{dir}"
    super
  end
...

Results

Use a subfolder for your module and keep it clean.

  • public/admin/images
  • public/admin/javascripts
  • public/admin/stylesheets

Your stylesheets get a little cleaner and…

  background: url(../images/frobnicators-background.png);

And so do your views…

image_tag(‘frobnicators.png’)

You can almost go on thinking the module is a separate app. No more intermingled assets.

Time Ago Method for Ruby on Rails

Posted by acts_as_flinn Tue, 10 Apr 2007 22:27:00 GMT

I recently searched for a Ruby or Ruby on Rails method to display date and time ago ala Typo, ex: “Posted 10 minutes ago”. As it turns out Typo uses Javascript to handle this.

Typo’s js_distance_of_time_in_words_to_now

Typo uses the method js_distance_of_time_in_words_to_now which results in something like this:

Posted by <cite>acts_as_flinn</cite>
<abbr class="published" title="2007-04-05T12:37:00-04:00"><span class="typo_date" title="Thu, 05 Apr 2007 16:37:00 GMT">Thu, 05 Apr 2007 16:37:00 GMT</span></abbr>

This is then parsed out to the users local time using javascript.

Rails Time Conversion API

I was surprised that Rails didn’t include something like this out of the box. The API docs refer to a number of convenience methods for handling time like since, ago, months_ago, years_ago, etc. but these don’t do what I want, instead the may it easy to use Numeric time conversions, for example: 5.month_ago returns a Time object set to 5 months ago.

Time as an adjective

What I really needed was time described in human readable terms with an adjective. Like 5 days ago, 10 minutes ago, 3 years ago, etc. So I googled around and found a reference to a timeago module written for Drupal http://zertox.com/topic/drupal/drupal_module_time_ago. I ported it to Ruby and placed it in my application helper, and it seems to work as intended.

# options
# :start_date, sets the time to measure against, defaults to now
# :later, changes the adjective and measures time forward
# :round, sets the unit of measure 1 = seconds, 2 = minutes, 3 hours, 4 days, 5 weeks, 6 months, 7 years (yuck!)
# :max_seconds, sets the maximimum practical number of seconds before just referring to the actual time
# :date_format, used with <tt>to_formatted_s<tt>
def timeago(original, options = {})
  start_date = options.delete(:start_date) || Time.now
  later = options.delete(:later) || false
  round = options.delete(:round) || 7
  max_seconds = options.delete(:max_seconds) || 32556926
  date_format = options.delete(:date_format) || :default

  # array of time period chunks
  chunks = [
    [60 * 60 * 24 * 365 , "year"],
    [60 * 60 * 24 * 30 , "month"],
    [60 * 60 * 24 * 7, "week"],
    [60 * 60 * 24 , "day"],
    [60 * 60 , "hour"],
    [60 , "minute"],
    [1 , "second"]
  ]

  if later
    since = original.to_i – start_date.to_i
  else
    since = start_date.to_i – original.to_i
  end
  time = []

  if since < max_seconds
    # Loop trough all the chunks
    totaltime = 0

    for chunk in chunks[0..round]
      seconds    = chunk[0]
      name       = chunk[1]

      count = ((since – totaltime) / seconds).floor
      time << pluralize(count, name) unless count == 0

      totaltime += count * seconds
    end

    if time.empty?
      "less than a #{chunks[round-1][1]} ago"
    else
      "#{time.join(’, ‘)} #{later ? ‘later’ : ‘ago’}"
    end
  else
    original.to_formatted_s(date_format)
  end
end

This yields results like 1 week, 18 hours ago. I think it works pretty well but I wanted it a little more vague like how Typo does it, so I ported the Typo methods from Javascript to Ruby.

Ruby on Rails Time Ago

  # options
  # :start_date, sets the time to measure against, defaults to now
  # :date_format, used with <tt>to_formatted_s<tt>, default to :default
  def timeago(time, options = {})
    start_date = options.delete(:start_date) || Time.new
    date_format = options.delete(:date_format) || :default
    delta_minutes = (start_date.to_i – time.to_i).floor / 60
    if delta_minutes.abs <= (8724*60) # eight weeks… I’m lazy to count days for longer than that
      distance = distance_of_time_in_words(delta_minutes);
      if delta_minutes < 0
        "#{distance} from now"
      else
        "#{distance} ago"
      end
    else
      return "on #{system_date.to_formatted_s(date_format)}"
    end
  end

  def distance_of_time_in_words(minutes)
    case
      when minutes < 1
        "less than a minute"
      when minutes < 50
        pluralize(minutes, "minute")
      when minutes < 90
        "about one hour"
      when minutes < 1080
        "#{(minutes / 60).round} hours"
      when minutes < 1440
        "one day"
      when minutes < 2880
        "about one day"
      else
        "#{(minutes / 1440).round} days"
    end
  end

The result is a nice vague string that rounds to the nearest unit of measure without being overly specific.

Enjoy!