Project

has_easy

0.01
No commit activity in last 3 years
No release in over 3 years
Easy access and creation of "has many" relationships for ActiveRecord models. It uses a "vertical table" so schema changes aren't necessary when you add fields. Use this plugin to add preferences, options, flags, etc to your models
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 1.0.0
~> 1.6.4
>= 0

Runtime

>= 3.0
 Project Readme

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¶ ↑

In your Gemfile:

gem "has_easy"

At the command prompt:

rails g has_easy_migration
rake db:migrate

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