has_easy 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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