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!

Rails Automatic Scoping ala Userstamp

Posted by acts_as_flinn Sun, 11 Mar 2007 08:14:00 GMT

I am working on an app to keep track of my customer base, projects, tasks, communications with customers, and some other functionality that you might find in CRM & Project Management apps. So I have a simple interface with a select box of all customers which I have placed in the header. Selecting a customer there acts as a global filter meaning it should do the following:

  1. Limit the results I get in my lists of projects, contact reports, people, and tasks so that I can focus on one customer at a time.
  2. I would also like to be able to leave that variable empty so that if I need to look at complete lists I can.
  3. One last thing, it should do this without adding tons of code all over the place and without brute force.

Userstamp Method

The Userstamp plugin gives us a good example of setting a current_user variable using the application controller and a session variable.

class User < ActiveRecord::Base
  cattr_accessor :current_user
end

The following code allows our models to refer to the User model because the session variable cannot be accessed from within models.

class ApplicationController < ActionController::Base
  before_filter do |c|
    User.current_user = User.find(c.session[:user].id) unless c.session[:user].nil?
  end
end

Well, in my case I am not working with a User but a Customer. So we’ll take what we’ve learned then apply it. Since my method is a cheap and easy ripoff of Userstamp I’ll name my method Trampstamp (because it’s cheap and easy).

Trampstamp Method

In my method the first thing that needs to be setup is the model to receive the current_customer. I’ll set that up in my Customer class.

class Customer < ActiveRecord::Base
  cattr_accessor :current_customer

  has_many :people
  has_many :projects

  ...

Now I need a way to store that value.

class CustomersController < ApplicationController
  def filter
    if params[:customer_id]
      session[:customer] = Customer.find(params[:customer_id])
    else
      session[:customer] = nil
    end
    redirect_to :back
  end

  ...

And a way to retrieve the session value and set the current customer.

class ApplicationController < ActionController::Base
  before_filter do |c|
    Customer.current_customer = c.session[:customer] unless c.session[:customer].nil?
  end

  ...

And finally a way to limit my search results without having to add conditions all over the application.

class Project < ActiveRecord::Base
  belongs_to :customer

  def self.find(*args)
    unless Customer.current_customer.nil?
      self.with_scope(:find => { :conditions => [‘customer_id = ?’, Customer.current_customer.id] }) { super }
    else
      super
    end
  end
end

Result – Judo Style automatic scoping

When I have a current_customer set,
Project.find(:all)
will yield the following query:
SELECT * FROM projects WHERE (customer_id = 1)

When no current_customer is set, it is business as usual. I suppose it would be really easy to add another method to automatically add :customer_id to project.create, and others. Maybe I’ll release that as a plugin so it’s even DRYer than this.

Here is the complete contact report list with no customer set.

This is the same list with the customer set.


ss_blog_claim=746d258dc975cb7923cc57154dbf1d71