has_easy 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,306 @@
1
+ Easy access and creation of "has many" relationships.
2
+
3
+ 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.
4
+
5
+ ==Installation
6
+ In your Gemfile:
7
+
8
+ gem "has_easy"
9
+
10
+ At the command prompt:
11
+ rails g has_easy_migration
12
+ rake db:migrate
13
+
14
+ ==Example
15
+
16
+ class User < ActiveRecord::Base
17
+ has_easy :preferences do |p|
18
+ p.define :color
19
+ p.define :theme
20
+ end
21
+ has_easy :flags do |f|
22
+ f.define :is_admin
23
+ f.define :is_spammer
24
+ end
25
+ end
26
+
27
+ user = User.new
28
+
29
+ # hash like access
30
+ user.preferences[:color] = 'red'
31
+ user.preferences[:color] # => 'red'
32
+
33
+ # object like access
34
+ user.preferences.theme? # => false, shorthand for !!user.preferences.theme
35
+ user.preferences.theme = "savage thunder"
36
+ user.preferences.theme # => "savage thunder"
37
+ user.preferences.theme? # => true
38
+
39
+ # easy access for form inputs
40
+ user.flags_is_admin? # => false, shorthand for !!user.flags_is_admin
41
+ user.flags_is_admin = true
42
+ user.flags_is_admin # => true
43
+ user.flags_is_admin? # => true
44
+
45
+ # save user's preferences
46
+ user.preferences.save # will trickle down validation errors to user
47
+ user.errors.empty? # hopefully true
48
+
49
+ # save user's flags
50
+ user.flags.save! # will raise exception on validation errors
51
+
52
+
53
+ ==Advanced Usage
54
+ There are a lot of options that you can use with has_easy:
55
+ * aliasing
56
+ * default values
57
+ * inheriting default values from parent associations
58
+ * calculated default values
59
+ * type checking values
60
+ * validating values
61
+ * preprocessing values
62
+ In this section, we'll go over how to use each option and explain why it's useful.
63
+
64
+ ===:alias and :aliases
65
+ These options go on the has_easy method call and specify alternate ways of invoking the association.
66
+ class User < ActiveRecord::Base
67
+ has_easy :preferences, :aliases => [:prefs, :options] do |p|
68
+ p.define :likes_cheese
69
+ end
70
+ has_easy :flags, :alias => :status do |p|
71
+ p.define :is_admin
72
+ end
73
+ end
74
+
75
+ user.preferences.likes_cheese = 'yes'
76
+ user.prefs.likes_cheese => 'yes'
77
+ user.options_likes_cheese => 'yes'
78
+ user.prefs[:likes_cheese] => 'yes'
79
+ user.options.likes_cheese? => true
80
+ ...etc...
81
+
82
+ ===:default
83
+ Very simple. It does what you think it does.
84
+ class User < ActiveRecord::Base
85
+ has_easy :options do |p|
86
+ p.define :gender, :default => 'female'
87
+ end
88
+ end
89
+
90
+ User.new.options.gender # => 'female'
91
+
92
+ ===:default_through
93
+ Allows the model to inherit it's default value from an association.
94
+ class Client < ActiveRecord::Base
95
+ has_many :users
96
+ has_easy :options do |p|
97
+ p.define :gender, :default => 'male'
98
+ end
99
+ end
100
+ class User < ActiveRecord::Base
101
+ belongs_to :client
102
+ has_easy :options do |p|
103
+ p.define :gender, :default_through => :client, :default => 'female'
104
+ end
105
+ end
106
+
107
+ client = Client.create
108
+ user = client.users.create
109
+ user.options.gender # => 'male'
110
+
111
+ client.options.gender = 'asexual'
112
+ client.options.save
113
+ user.client(true) # reload association
114
+ user.options.gender # => 'asexual'
115
+
116
+ User.new.options.gender => 'female'
117
+
118
+
119
+ ===:default_dynamic
120
+ Allows for calculated default values.
121
+ class User < ActiveRecord::Base
122
+ has_easy 'prefs' do |t|
123
+ t.define :likes_cheese, :default_dynamic => :defaults_to_like_cheese
124
+ t.define :is_dumb, :default_dynamic => Proc.new{ |user| user.dumb_post_count > 10 }
125
+ end
126
+
127
+ def defaults_to_like_cheese
128
+ cheesy_post_count > 10
129
+ end
130
+ end
131
+
132
+ user = User.new :cheesy_post_count => 5
133
+ user.prefs.likes_cheese? => false
134
+
135
+ user = User.new :cheesy_post_count => 11
136
+ user.prefs.likes_cheese? => true
137
+
138
+ user = User.new :dumb_post_count => 5
139
+ user.prefs.is_dumb? => false
140
+
141
+ user = User.new :dumb_post_count => 11
142
+ user.prefs.is_dumb? => true
143
+
144
+
145
+ ===:type_check
146
+ Allows type checking of values (for people who are into that).
147
+ class User < ActiveRecord::Base
148
+ has_easy :prefs do |p|
149
+ p.define :theme, :type_check => String
150
+ p.define :dollars, :type_check => [Fixnum, Bignum]
151
+ end
152
+ end
153
+
154
+ user.prefs.theme = 123
155
+ user.prefs.save! # ActiveRecord::InvalidRecord exception raised with message like:
156
+ # 'theme' for has_easy('prefs') failed type check
157
+
158
+ user.prefs.dollars = "hello world"
159
+ user.prefs.save
160
+ user.errors.empty? # => false
161
+ user.errors.on(:prefs) # => 'dollars' for has_easy('prefs') failed type check
162
+
163
+
164
+ ===:validate
165
+ 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:
166
+ 1. return false
167
+ 2. raise a HasEasy::ValidationError
168
+ 3. return an array of custom validation error messages
169
+ class User < ActiveRecord::Base
170
+ has_easy :prefs do |p|
171
+ p.define :foreground, :validate => ['red', 'blue', 'green']
172
+ p.define :background, :validate => Proc.new{ |value| %w[black white grey].include?(value) }
173
+ p.define :midground, :validate => :midground_validator
174
+ end
175
+ def midground_validator(value)
176
+ return ["msg1", msg2] unless %w[yellow brown purple].include?(value)
177
+ end
178
+ end
179
+
180
+ user.prefs.foreground = 'yellow'
181
+ user.prefs.save! # ActiveRecord::InvalidRecord exception raised with message like:
182
+ # 'theme' for has_easy('prefs') failed validation
183
+
184
+ user.prefs.background = "pink"
185
+ user.prefs.save
186
+ user.errors.empty? => false
187
+ user.errors.on(:prefs) => 'background' for has_easy('prefs') failed validation
188
+
189
+ user.prefs.midground = "black"
190
+ user.prefs.save
191
+ user.errors.on(:prefs)[0] => "msg1"
192
+ user.errors.on(:prefs)[1] => "msg2"
193
+
194
+
195
+ ===:preprocess
196
+ 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. <tt>prefs_likes_cheese=</tt>, not <tt>prefs.likes_cheese=</tt> or <tt>prefs[:likes_cheese]=</tt>.
197
+ class User < ActiveRecord::Base
198
+ has_easy :prefs do |p|
199
+ p.define :likes_cheese, :validate => [true, false],
200
+ :preprocess => Proc.new{ |value| ['true', 'yes'].include?(value) ? true : false }
201
+ end
202
+ end
203
+
204
+ user.prefs.likes_cheese = 'yes' # :preprocess NOT invoked; it only applies to underscore accessors!!
205
+ user.prefs.likes_cheese
206
+ => 'yes'
207
+ user.prefs.save! # exception, validation failed
208
+
209
+ user.prefs_likes_cheese = 'yes' # :preprocess invoked
210
+ user.prefs.likes_cheese
211
+ => true
212
+ user.prefs.save! # no exception
213
+
214
+
215
+ ===:postprocess
216
+ 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. <tt>prefs_likes_cheese</tt>, not <tt>prefs.likes_cheese</tt> or <tt>prefs[:likes_cheese]</tt>.
217
+ class User < ActiveRecord::Base
218
+ has_easy :prefs do |p|
219
+ p.define :likes_cheese, :validate => [true, false],
220
+ :postprocess => Proc.new{ |value| value ? 'yes' : 'no' }
221
+ end
222
+ end
223
+
224
+ user.prefs.likes_cheese = true
225
+ user.prefs.likes_cheese # :postprocess NOT invoked, it only applies to underscore accessors
226
+ => true
227
+ user.prefs_likes_cheese # :postprocess invoked
228
+ => 'yes'
229
+
230
+ ==Using with Forms
231
+ Suppose you have a <tt>has_easy</tt> field defined as a boolean and you want to use it with a checkbox in <tt>form_for</tt>.
232
+
233
+ (model)
234
+
235
+ class User < ActiveRecord::Base
236
+ has_easy :prefs do |p|
237
+ p.define :likes_cheese, :type_check => [TrueClass, FalseClass],
238
+ :preprocess => Proc.new{ |value| value == 'yes' },
239
+ :postprocess => Proc.new{ |value| value ? 'yes' : 'no' }
240
+ end
241
+ end
242
+
243
+ (view)
244
+
245
+ <% form_for(@user) do |f| %>
246
+ <%= f.check_box 'user', 'prefs_likes_cheese', {}, 'yes', 'no' %> # invokes @user.prefs_likes_cheese which does the :postprocess
247
+ <% end %>
248
+
249
+ (controller)
250
+
251
+ @user.update_attributes(params[:user]) # invokes @user.prefs_likes_cheese= which does the :preprocess
252
+ @user.prefs.save
253
+ @user.prefs.likes_cheese
254
+ => true or false
255
+ @user.prefs_likes_cheese # remember, only underscore accessors invoke the :preprocess and :postprocess options
256
+ => 'yes' or 'no'
257
+
258
+ The general idea is that we make the form use <tt>prefs_likes_cheese=</tt> and <tt>prefs_likes_cheese</tt> accessors which in turn use the :preprocess and :postprocess options. Then in our normal code, we use <tt>prefs.likes_cheese</tt> or <tt>prefs[:likes_cheese]</tt> accessors to get our expected boolean values.
259
+
260
+ ==Missing Features
261
+
262
+ ===Autovivification
263
+ For when we want to use fields without having to define them first.
264
+ class User < ActiveRecord::Base
265
+ has_easy :prefs, :autovivify => true do |p|
266
+ p.define :likes_cheese, :default => 'yes'
267
+ end
268
+ end
269
+
270
+ user.prefs.likes_cheese => 'yes'
271
+ user.prefs.likes_pizza => nil
272
+ user.prefs.likes_pizza = true
273
+ user.prefs.likes_pizza => true
274
+
275
+
276
+ ===Scoping to other models
277
+ 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.
278
+ class User < ActiveRecord::Base
279
+ has_easy :prefs do |p|
280
+ p.define :subscribed, :scoped => Post
281
+ p.define :color, :scoped => [Car, Motorcycle] # polymorphic but must be Car or Motorcycle
282
+ p.define :hair_color, :scoped => true # polymorphic no restrictions
283
+ p.define :likes_cheese, :scoped => [Food, NilClass] # scoped and not scoped at the same time
284
+ end
285
+ end
286
+
287
+ post = Post.find :first, :conditions => {:topic => 'rails'}
288
+ me.prefs.subscribed? :to => post
289
+ => true
290
+
291
+ vette = Car.find :first, :conditions => {:model => 'corvette'}
292
+ me.prefs.color :for => vette
293
+ => 'black'
294
+
295
+ gf = Girl.find :first, :conditions => {:name => 'aimee'}
296
+ me.prefs.hair_color :on => gf
297
+ => 'brown'
298
+
299
+ watermelon = Food.find :first, :conditions => {:kind => 'watermelon'}
300
+ my.prefs.likes_cheese? # not scoped; do I like cheese in general?
301
+ => true
302
+ my.prefs.likes_cheese? :on => watermelon # scoped; do I like cheese on watermelon?
303
+ => false
304
+
305
+
306
+ Copyright (c) 2008 Christopher J. Bottaro <cjbottaro@alumni.cs.utexas.edu>, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "has_easy"
18
+ gem.homepage = "http://github.com/jwigal/has_easy"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{TODO: one-line summary of your gem}
21
+ gem.description = %Q{TODO: longer description of your gem}
22
+ gem.email = "jeff@assignr.com"
23
+ gem.authors = ["Jeff Wigal"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rake/testtask'
29
+ Rake::TestTask.new(:test) do |test|
30
+ test.libs << 'lib' << 'test'
31
+ test.pattern = 'test/**/test_*.rb'
32
+ test.verbose = true
33
+ end
34
+
35
+ require 'rcov/rcovtask'
36
+ Rcov::RcovTask.new do |test|
37
+ test.libs << 'test'
38
+ test.pattern = 'test/**/test_*.rb'
39
+ test.verbose = true
40
+ test.rcov_opts << '--exclude "gems/*"'
41
+ end
42
+
43
+ task :default => :test
44
+
45
+ require 'rdoc/task'
46
+ Rake::RDocTask.new do |rdoc|
47
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
48
+
49
+ rdoc.rdoc_dir = 'rdoc'
50
+ rdoc.title = "has_easy #{version}"
51
+ rdoc.rdoc_files.include('README*')
52
+ rdoc.rdoc_files.include('lib/**/*.rb')
53
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.9.0
File without changes
@@ -0,0 +1,19 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+
4
+ class HasEasyMigrationGenerator < Rails::Generators::Base
5
+ include Rails::Generators::Migration
6
+ def self.source_root
7
+ @source_root ||= File.join(File.dirname(__FILE__), 'templates')
8
+ end
9
+
10
+ # migration_template 'has_easy_migration.rb'
11
+ def self.next_migration_number(path)
12
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
13
+ end
14
+
15
+ def create_model_file
16
+ migration_template "has_easy_migration.rb", "db/migrate/#{migration_number}create_has_easy_migration.rb"
17
+ end
18
+
19
+ end
@@ -0,0 +1,16 @@
1
+ class CreateHasEasyMigration < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :has_easy_things do |t|
4
+ t.string :model_type, :null => false
5
+ t.integer :model_id, :null => false
6
+ t.string :context
7
+ t.string :name, :null => false
8
+ t.string :value
9
+ t.timestamps
10
+ end
11
+ end
12
+
13
+ def self.down
14
+ drop_table :has_easy_things
15
+ end
16
+ end
@@ -0,0 +1,53 @@
1
+ module Izzle
2
+ module HasEasy
3
+ module AssocationExtension
4
+
5
+ def save
6
+ do_save(false)
7
+ end
8
+
9
+ def save!
10
+ do_save(true)
11
+ end
12
+
13
+ def []=(name, value)
14
+ proxy_association.owner.set_has_easy_thing(proxy_association.reflection.name, name, value)
15
+ end
16
+
17
+ def [](name)
18
+ proxy_association.owner.get_has_easy_thing(proxy_association.reflection.name, name)
19
+ end
20
+
21
+ def valid?
22
+ valid = true
23
+ proxy_association.target.each do |thing|
24
+ thing.model_cache = proxy_association.owner
25
+ unless thing.valid?
26
+ thing.errors.each{ |attr, msg| proxy_association.owner.errors.add(proxy_association.reflection.name, msg) }
27
+ valid = false
28
+ end
29
+ end
30
+ valid
31
+ end
32
+
33
+ private
34
+
35
+ def do_save(with_bang)
36
+ success = true
37
+ proxy_association.target.each do |thing|
38
+ next if !thing.changed?
39
+ thing.model_cache = proxy_association.owner
40
+ if with_bang
41
+ thing.save!
42
+ elsif thing.save == false
43
+ # delegate the errors to the proxy owner
44
+ thing.errors.each { |attr,msg| proxy_association.owner.errors.add(proxy_association.reflection.name, msg) }
45
+ success = false
46
+ end
47
+ end
48
+ success
49
+ end
50
+
51
+ end
52
+ end
53
+ end