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/CHANGELOG +16 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +100 -0
- data/MIT-LICENSE +20 -0
- data/README +306 -0
- data/README.rdoc +306 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/lib/generators/has_easy_migration/USAGE +0 -0
- data/lib/generators/has_easy_migration/has_easy_migration_generator.rb +19 -0
- data/lib/generators/has_easy_migration/templates/has_easy_migration.rb +16 -0
- data/lib/has_easy/association_extension.rb +53 -0
- data/lib/has_easy/configurator.rb +87 -0
- data/lib/has_easy/definition.rb +56 -0
- data/lib/has_easy/errors.rb +4 -0
- data/lib/has_easy/helpers.rb +11 -0
- data/lib/has_easy.rb +133 -0
- data/lib/has_easy_thing.rb +49 -0
- data/lib/tasks/has_easy_tasks.rake +4 -0
- data/test/test_has_easy.rb +337 -0
- metadata +167 -0
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
|