has_easy
has_easy copied to clipboard
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, o...
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:
- 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 (c) 2008 Christopher J. Bottaro [email protected], released under the MIT license