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..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..gender # => 'male'
client..gender = 'asexual'
client..save
user.client(true) # reload association
user..gender # => 'asexual'
User.new..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:
-
return false
-
raise a HasEasy::ValidationError
-
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 <[email protected]>, released under the MIT license