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.
¶ ↑
Installationgit 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
¶ ↑
Exampleclass 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 UsageThere 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 :aliasesThese 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...
¶ ↑
:defaultVery 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_throughAllows 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_dynamicAllows 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_checkAllows 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
¶ ↑
:validateMake 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"
¶ ↑
:preprocessAlter 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
¶ ↑
:postprocessAlter 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 FormsSuppose 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¶ ↑
AutovivificationFor 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 modelsEhh, 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