has_easy icon indicating copy to clipboard operation
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:

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