Flagger

  • Current version: 0.9.5
  • Release date: 31st May 2007

Installation

To get the latest release, open a command prompt in your Rails project directory and type (line wraps marked »):

script/plugin install »
  http://svn.jcoglan.com/flagger/tags/0.9.5/flagger

Or, to get the latest development version:

script/plugin install »
  http://svn.jcoglan.com/flagger/trunk/flagger

Introduction

Do you often find yourself writing repetitive collections of methods on your models that look like:

  class Order < ActiveRecord::Base
    def self.paid
      find_all_by_paid(true)
    end

    def self.unpaid
      find_all_by_paid(false)
    end

    def mark_as_paid
      update_attribute(:paid, true)
      # related tasks
    end

    def mark_as_unpaid
      update_attribute(:paid, false)
      # related tasks
    end
  end

This is good practise, as it keeps your column names wrapped up in the model in case they need changing, rather than littering your controllers (or other models) with them. And it increases code legibility, which is also a Good Thing.

The Flagger plugin allows ActiveRecord::Base to give you these methods without you having to write any code. It uses method_missing hooks to respond to methods not caught by ActiveRecord::Base‘s built-in method_missing code. It is designed to be used with boolean attributes, that is, attributes that contain true/false, 1/0 or "1"/"0" depending on column type. (Although, it is generally not a good idea to use string/text columns for this type of data. You may encounter problems if you attempt to do so, depending on which Rails version you are using.)

Boolean finders

Flagger allows you to use dynamic finders on boolean attributes without using find_all_by_. So, using the above example, you can happily call Order.paid to get all your paid orders, without defining the method yourself. In addition, you can call Order.unpaid, and you will get back all the orders for which paid == false. The full list of allowed prefixes is:

  • in, im, un, non, dis, de, ir, and not_

If your model has an attribute whose name already begins with one of these, Flagger will take this into account. For example, TallStory.implausible returns all the TallStory records for which implausible == true. You can chop off the prefix as well: TallStory.plausible does what you‘d expect.

Only one prefix will be added to or removed from the attribute name you use to try and find matching attribute names. Fact.undeniable works if the Fact model has an attribute called undeniable, deniable, inundeniable, imundeniable … or not_undeniable. Attribute names are always checked in this order, and Flagger will use the first one that exists. If no attribute match can be found, the exception raised by ActiveRecord::Base‘s built-in method_missing code is reraised.

Attribute names can be chained together in a similar fashion to that allowed by find_by_ and friends. You can use and and but to separate names, so you could write:

  orders = Order.paid_and_delivered
  orders.first.paid?
  #=> true
  orders.first.delivered?
  #=> true

  martinis = Cocktail.shaken_but_not_stirred
  martinis.first.shaken?
  #=> true
  martinis.first.stirred?
  #=> false

Dynamic markers

Attribute-based markers perform the converse task to boolean finders - they set boolean attributes rather than retrieving based on them. They mark (flag) records as having certain properties. Their syntax is very similar to that for finders:

  order = Order.find(:all).first
  order.mark_as_paid
  order.paid?
  #=> true
  order.mark_as_delivered_but_not_signed_for
  order.delivered?
  #=> true
  order.signed_for?
  #=> false

These methods do not save the attributes to the database. If you want to save the attributes, end the method call with a !.

mark_as_ methods detect the column type for each attribute and set values accordingly. true/false are used for :boolean fields, 1/0 for numeric fields, and "1"/"0" for :string/:text fields.

To perform tasks related to flagging, you can define after_mark_as_ methods on your models. Each callback can include one attribute name (with or without prefixes as appropriate) and can end with a ! to denote whether it should be called on database saves or not. For example, Order#after_mark_as_unpaid! will be called after mark_as_unpaid!, mark_as_not_paid! and mark_as_accepted_but_not_paid!, but not after mark_as_unpaid (no exclamation mark). All the existing callbacks that apply to a mark_as_ call will be called, but only if a change has been made to the relevant attribute.

Compatibility

Flagger has been tested successfully using MySQL 5 with Rails 1.1.6 and 1.2.0 through 1.2.3. Bug reports and test reports for other databases are welcome - my email address is at the end of this file.

Notes

Flagger is an adjusted version of a patch I submitted to Rails that didn‘t make it into the core. The source has been modified from that version in a few places. Thanks to tomafro for pointing me in the right direction and plugging holes in my Ruby/Rails knowledge.

Clearly this plugin will not cover all possible use cases for flaggable models. It is meant to try and provide some generally useful functionality out of the box, and to eliminate the need to write code for the simpler cases. If it doesn‘t quite do what you need, you can always roll your own methods to avoid hitting method_missing.

One potentially useful addition would be support for OR-based finder conditions, as in Lorry.red_or_yellow, although I suspect that including that would be excessively complicating things - how would one handle Lorry.red_and_yellow_or_green_but_not_blue? Does that mean (red && yellow) || (green && !blue), or red && (yellow || green) && !blue?

Flagger is (c) 2007 James Coglan and is released under the terms of the MIT license.

Contact: james at jcoglan dot com