Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Easy access and creation of "has many" relationships for ActiveRecord models. Use this plugin to add preferences, options, flags, etc to your models.
Branch: master
Pull request Compare This branch is even with mumboe:master.

Fetching latest commit…

Cannot retrieve the latest commit at this time

Failed to load latest commit information.
generators/has_easy_migration
lib
test
CHANGELOG
MIT-LICENSE
README
README.rdoc
Rakefile
init.rb
install.rb
uninstall.rb

README.rdoc

Easy access and creation of “has many” relationships.

What's the difference between flags, preferences and options? Nothing really, they are just “has many” relationships. So why should I install a separate plugin for each one? This plugin can be used to add preferences, flags, options, etc to any model.

Installation

git clone git://github.com/cjbottaro/has_easy.git vendor/plugins/has_easy
script/generate has_easy_migration create_has_easy_things
rake db:migrate
rake db:test:prepare
cd vendor/plugins/has_easy
rake test

Example

class User < ActiveRecord::Base
  has_easy :preferences do |p|
    p.define :color
    p.define :theme
  end
  has_easy :flags do |f|
    f.define :is_admin
    f.define :is_spammer
  end
end

user = User.new

# hash like access
user.preferences[:color] = 'red'
user.preferences[:color] # => 'red'

# object like access
user.preferences.theme? # => false, shorthand for !!user.preferences.theme
user.preferences.theme = "savage thunder"
user.preferences.theme # => "savage thunder"
user.preferences.theme? # => true

# easy access for form inputs
user.flags_is_admin? # => false, shorthand for !!user.flags_is_admin
user.flags_is_admin = true
user.flags_is_admin # => true
user.flags_is_admin? # => true

# save user's preferences
user.preferences.save # will trickle down validation errors to user
user.errors.empty? # hopefully true

# save user's flags
user.flags.save! # will raise exception on validation errors

Advanced Usage

There are a lot of options that you can use with has_easy:

  • aliasing

  • default values

  • inheriting default values from parent associations

  • calculated default values

  • type checking values

  • validating values

  • preprocessing values

In this section, we'll go over how to use each option and explain why it's useful.

:alias and :aliases

These options go on the has_easy method call and specify alternate ways of invoking the association.

class User < ActiveRecord::Base
  has_easy :preferences, :aliases => [:prefs, :options] do |p|
    p.define :likes_cheese
  end
  has_easy :flags, :alias => :status do |p|
    p.define :is_admin
  end
end

user.preferences.likes_cheese = 'yes'
user.prefs.likes_cheese => 'yes'
user.options_likes_cheese => 'yes'
user.prefs[:likes_cheese] => 'yes'
user.options.likes_cheese? => true
...etc...

:default

Very simple. It does what you think it does.

class User < ActiveRecord::Base
  has_easy :options do |p|
    p.define :gender, :default => 'female'
  end
end

User.new.options.gender # => 'female'

:default_through

Allows the model to inherit it's default value from an association.

class Client < ActiveRecord::Base
  has_many :users
  has_easy :options do |p|
    p.define :gender, :default => 'male'
  end
end
class User < ActiveRecord::Base
  belongs_to :client
  has_easy :options do |p|
    p.define :gender, :default_through => :client, :default => 'female'
  end
end

client = Client.create
user = client.users.create
user.options.gender # => 'male'

client.options.gender = 'asexual'
client.options.save
user.client(true) # reload association
user.options.gender # => 'asexual'

User.new.options.gender => 'female'

:default_dynamic

Allows for calculated default values.

class User < ActiveRecord::Base
  has_easy 'prefs' do |t|
    t.define :likes_cheese, :default_dynamic => :defaults_to_like_cheese
    t.define :is_dumb, :default_dynamic => Proc.new{ |user| user.dumb_post_count > 10 }
  end

  def defaults_to_like_cheese
    cheesy_post_count > 10
  end
end

user = User.new :cheesy_post_count => 5
user.prefs.likes_cheese? => false

user = User.new :cheesy_post_count => 11
user.prefs.likes_cheese? => true

user = User.new :dumb_post_count => 5
user.prefs.is_dumb? => false

user = User.new :dumb_post_count => 11
user.prefs.is_dumb? => true

:type_check

Allows type checking of values (for people who are into that).

class User < ActiveRecord::Base
  has_easy :prefs do |p|
    p.define :theme, :type_check => String
    p.define :dollars, :type_check => [Fixnum, Bignum]
  end
end

user.prefs.theme = 123
user.prefs.save! # ActiveRecord::InvalidRecord exception raised with message like:
                 # 'theme' for has_easy('prefs') failed type check

user.prefs.dollars = "hello world"
user.prefs.save
user.errors.empty? # => false
user.errors.on(:prefs) # => 'dollars' for has_easy('prefs') failed type check

:validate

Make sure that values fit some kind of criteria. If you use a Proc or name a method with a Symbol to do validation, there are three ways to specify failure:

  1. return false

  2. raise a HasEasy::ValidationError

  3. return an array of custom validation error messages

class User < ActiveRecord::Base
  has_easy :prefs do |p|
    p.define :foreground, :validate => ['red', 'blue', 'green']
    p.define :background, :validate => Proc.new{ |value| %w[black white grey].include?(value) }
    p.define :midground,  :validate => :midground_validator
  end
  def midground_validator(value)
    return ["msg1", msg2] unless %w[yellow brown purple].include?(value)
  end
end

user.prefs.foreground = 'yellow'
user.prefs.save! # ActiveRecord::InvalidRecord exception raised with message like:
                 # 'theme' for has_easy('prefs') failed validation

user.prefs.background = "pink"
user.prefs.save
user.errors.empty? => false
user.errors.on(:prefs) => 'background' for has_easy('prefs') failed validation

user.prefs.midground = "black"
user.prefs.save
user.errors.on(:prefs)[0] => "msg1"
user.errors.on(:prefs)[1] => "msg2"

:preprocess

Alter the value before it goes through type checking and/or validation. This is useful when working with forms and boolean values. CAREFUL!! This option only applies to the underscore accessors, i.e. prefs_likes_cheese=, not prefs.likes_cheese= or prefs[:likes_cheese]=.

class User < ActiveRecord::Base
  has_easy :prefs do |p|
    p.define :likes_cheese, :validate => [true, false],
                            :preprocess => Proc.new{ |value| ['true', 'yes'].include?(value) ? true : false }
  end
end

user.prefs.likes_cheese = 'yes' # :preprocess NOT invoked; it only applies to underscore accessors!!
user.prefs.likes_cheese
=> 'yes'
user.prefs.save! # exception, validation failed

user.prefs_likes_cheese = 'yes' # :preprocess invoked
user.prefs.likes_cheese
=> true
user.prefs.save! # no exception

:postprocess

Alter the value when it is read. This is useful when working with forms and boolean values. CAREFUL!! This option only applies to the underscore accessors, i.e. prefs_likes_cheese, not prefs.likes_cheese or prefs[:likes_cheese].

class User < ActiveRecord::Base
  has_easy :prefs do |p|
    p.define :likes_cheese, :validate => [true, false],
                            :postprocess => Proc.new{ |value| value ? 'yes' : 'no' }
  end
end

user.prefs.likes_cheese = true
user.prefs.likes_cheese # :postprocess NOT invoked, it only applies to underscore accessors
=> true
user.prefs_likes_cheese # :postprocess invoked
=> 'yes'

Using with Forms

Suppose you have a has_easy field defined as a boolean and you want to use it with a checkbox in form_for.

(model)

class User < ActiveRecord::Base
  has_easy :prefs do |p|
    p.define :likes_cheese, :type_check => [TrueClass, FalseClass],
                            :preprocess => Proc.new{ |value| value == 'yes' },
                            :postprocess => Proc.new{ |value| value ? 'yes' : 'no' }
  end
end

(view)

<% form_for(@user) do |f| %>
  <%= f.check_box 'user', 'prefs_likes_cheese', {}, 'yes', 'no' %> # invokes @user.prefs_likes_cheese which does the :postprocess
<% end %>

(controller)

@user.update_attributes(params[:user]) # invokes @user.prefs_likes_cheese= which does the :preprocess
@user.prefs.save
@user.prefs.likes_cheese
=> true or false
@user.prefs_likes_cheese # remember, only underscore accessors invoke the :preprocess and :postprocess options
=> 'yes' or 'no'

The general idea is that we make the form use prefs_likes_cheese= and prefs_likes_cheese accessors which in turn use the :preprocess and :postprocess options. Then in our normal code, we use prefs.likes_cheese or prefs[:likes_cheese] accessors to get our expected boolean values.

Missing Features

Autovivification

For when we want to use fields without having to define them first.

class User < ActiveRecord::Base
  has_easy :prefs, :autovivify => true do |p|
    p.define :likes_cheese, :default => 'yes'
  end
end

user.prefs.likes_cheese => 'yes'
user.prefs.likes_pizza => nil
user.prefs.likes_pizza = true
user.prefs.likes_pizza => true

Scoping to other models

Ehh, can't think of a way to describe this other than example. Also, the syntax is completely up in the air, there are so many different ways to do it, I have no idea which way to go with. Please tell me your ideas.

class User < ActiveRecord::Base
  has_easy :prefs do |p|
    p.define :subscribed, :scoped => Post
    p.define :color, :scoped => [Car, Motorcycle] # polymorphic but must be Car or Motorcycle
    p.define :hair_color, :scoped => true # polymorphic no restrictions
    p.define :likes_cheese, :scoped => [Food, NilClass] # scoped and not scoped at the same time
  end
end

post = Post.find :first, :conditions => {:topic => 'rails'}
me.prefs.subscribed? :to => post
=> true

vette = Car.find :first, :conditions => {:model => 'corvette'}
me.prefs.color :for => vette
=> 'black'

gf = Girl.find :first, :conditions => {:name => 'aimee'}
me.prefs.hair_color :on => gf
=> 'brown'

watermelon = Food.find :first, :conditions => {:kind => 'watermelon'}
my.prefs.likes_cheese? # not scoped; do I like cheese in general?
=> true
my.prefs.likes_cheese? :on => watermelon # scoped; do I like cheese on watermelon?
=> false

Copyright © 2008 Christopher J. Bottaro <cjbottaro@alumni.cs.utexas.edu>, released under the MIT license

Something went wrong with that request. Please try again.