josevalim-inherited_resources 0.3 → 0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +7 -0
- data/README +30 -1
- data/lib/inherited_resources/base.rb +1 -3
- data/lib/inherited_resources/belongs_to_helpers.rb +6 -30
- data/lib/inherited_resources/class_methods.rb +241 -4
- data/lib/inherited_resources/polymorphic_helpers.rb +47 -2
- data/lib/inherited_resources/url_helpers.rb +2 -1
- data/test/belongs_to_test.rb +66 -76
- data/test/class_methods_test.rb +111 -2
- data/test/optional_belongs_to_test.rb +190 -0
- data/test/polymorphic_test.rb +111 -0
- data/test/singleton_test.rb +83 -0
- data/test/url_helpers_test.rb +36 -4
- data/test/views/products/edit.html.erb +1 -0
- data/test/views/products/index.html.erb +1 -0
- data/test/views/products/new.html.erb +1 -0
- data/test/views/products/show.html.erb +1 -0
- metadata +10 -7
- data/lib/inherited_resources/belongs_to.rb +0 -227
- data/test/belongs_to_base_test.rb +0 -268
- data/test/polymorphic_base_test.rb +0 -282
- data/test/singleton_base_test.rb +0 -226
data/CHANGELOG
CHANGED
data/README
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Inherited Resources
|
2
2
|
License: MIT
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.4
|
4
4
|
|
5
5
|
You can also read this README in pretty html at the GitHub project Wiki page:
|
6
6
|
|
@@ -347,6 +347,35 @@ When using polymorphic associations, you get some free helpers:
|
|
347
347
|
Polymorphic controller is another great idea by James Golick and he also uses
|
348
348
|
that on resource_controller.
|
349
349
|
|
350
|
+
Optional belongs to
|
351
|
+
-------------------
|
352
|
+
|
353
|
+
Let's take another break from Projects. Let's suppose that we are now building
|
354
|
+
a store, which sell products.
|
355
|
+
|
356
|
+
On the website, we can show all products, but also products scoped to
|
357
|
+
categories, brands, users, etc. In this case case, the association is optional,
|
358
|
+
and we deal with it in the following way:
|
359
|
+
|
360
|
+
class ProductsController < InheritedResources::Base
|
361
|
+
belongs_to :category, :brand, :user, :polymorphic => true, :optional => true
|
362
|
+
end
|
363
|
+
|
364
|
+
This will handle all those urls properly:
|
365
|
+
|
366
|
+
/products/1
|
367
|
+
/categories/2/products/5
|
368
|
+
/brands/10/products/3
|
369
|
+
/user/13/products/11
|
370
|
+
|
371
|
+
This is treated as a special type of polymorphic associations, thus all helpers
|
372
|
+
are available. As you expect, when no parent is found, the helpers return:
|
373
|
+
|
374
|
+
parent? #=> false
|
375
|
+
parent_type #=> nil
|
376
|
+
parent_class #=> nil
|
377
|
+
parent #=> nil
|
378
|
+
|
350
379
|
Singletons
|
351
380
|
----------
|
352
381
|
|
@@ -161,7 +161,6 @@
|
|
161
161
|
# Let's require all needed files here. We are still on time to eager load
|
162
162
|
# everything on multithreaded environments.
|
163
163
|
require File.dirname(__FILE__) + '/base_helpers.rb'
|
164
|
-
require File.dirname(__FILE__) + '/belongs_to.rb'
|
165
164
|
require File.dirname(__FILE__) + '/belongs_to_helpers.rb'
|
166
165
|
require File.dirname(__FILE__) + '/class_methods.rb'
|
167
166
|
require File.dirname(__FILE__) + '/dumb_responder.rb'
|
@@ -176,12 +175,11 @@ module InheritedResources
|
|
176
175
|
unloadable
|
177
176
|
|
178
177
|
include InheritedResources::BaseHelpers
|
179
|
-
extend InheritedResources::BelongsTo
|
180
178
|
extend InheritedResources::ClassMethods
|
181
179
|
|
182
180
|
helper_method :collection_url, :collection_path, :resource_url, :resource_path,
|
183
181
|
:new_resource_url, :new_resource_path, :edit_resource_url, :edit_resource_path,
|
184
|
-
:resource, :collection, :resource_class
|
182
|
+
:resource, :collection, :resource_class, :parent?
|
185
183
|
|
186
184
|
def self.inherited(base)
|
187
185
|
base.class_eval do
|
@@ -4,10 +4,6 @@ module InheritedResources #:nodoc:
|
|
4
4
|
# Private helpers, you probably don't have to worry with them.
|
5
5
|
private
|
6
6
|
|
7
|
-
# Overwrites the parent? method defined in base_helpers.rb.
|
8
|
-
# This one always returns true since it's added when associations
|
9
|
-
# are defined.
|
10
|
-
#
|
11
7
|
def parent?
|
12
8
|
true
|
13
9
|
end
|
@@ -33,14 +29,13 @@ module InheritedResources #:nodoc:
|
|
33
29
|
# parents chain and returns the scoped association.
|
34
30
|
#
|
35
31
|
def end_of_association_chain
|
36
|
-
return resource_class unless parent?
|
37
|
-
|
38
32
|
chain = symbols_for_chain.inject(begin_of_association_chain) do |chain, symbol|
|
39
33
|
evaluate_parent(resources_configuration[symbol], chain)
|
40
34
|
end
|
41
35
|
|
42
|
-
|
36
|
+
return resource_class unless chain
|
43
37
|
|
38
|
+
chain = chain.send(method_for_association_chain) if method_for_association_chain
|
44
39
|
return chain
|
45
40
|
end
|
46
41
|
|
@@ -58,31 +53,12 @@ module InheritedResources #:nodoc:
|
|
58
53
|
singleton ? nil : resource_collection_name
|
59
54
|
end
|
60
55
|
|
61
|
-
# Maps parents_symbols to build association chain.
|
62
|
-
#
|
63
|
-
#
|
64
|
-
# params keys to see which polymorphic parent matches the given params.
|
56
|
+
# Maps parents_symbols to build association chain. In this case, it
|
57
|
+
# simply return the parent_symbols, however on polymorphic belongs to,
|
58
|
+
# it has some customization to deal with properly.
|
65
59
|
#
|
66
60
|
def symbols_for_chain
|
67
|
-
parents_symbols
|
68
|
-
if symbol == :polymorphic
|
69
|
-
params_keys = params.keys
|
70
|
-
|
71
|
-
key = polymorphic_symbols.find do |poly|
|
72
|
-
params_keys.include? resources_configuration[poly][:param].to_s
|
73
|
-
end
|
74
|
-
|
75
|
-
raise ScriptError, "Could not find param for polymorphic association.
|
76
|
-
The request params keys are #{params.keys.inspect}
|
77
|
-
and the polymorphic associations are
|
78
|
-
#{polymorphic_symbols.inspect}." if key.nil?
|
79
|
-
|
80
|
-
instance_variable_set('@parent_type', key.to_sym)
|
81
|
-
else
|
82
|
-
symbol
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
61
|
+
parents_symbols
|
86
62
|
end
|
87
63
|
|
88
64
|
end
|
@@ -1,3 +1,186 @@
|
|
1
|
+
# = belongs to
|
2
|
+
#
|
3
|
+
# This allows you to specify to belongs_to in your controller. You might use
|
4
|
+
# this when you are having nested resources in your routes:
|
5
|
+
#
|
6
|
+
# class TasksController < InheritedResources::Base
|
7
|
+
# belongs_to :project
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# This will do all magic assuming some defaults. It assumes that your URL to
|
11
|
+
# access those tasks are:
|
12
|
+
#
|
13
|
+
# /projects/:project_id/tasks
|
14
|
+
#
|
15
|
+
# But all defaults are configurable. The options are:
|
16
|
+
#
|
17
|
+
# * :parent_class => Allows you to specify what is the parent class.
|
18
|
+
#
|
19
|
+
# belongs_to :project, :parent_class => AdminProject
|
20
|
+
#
|
21
|
+
# * :class_name => Also allows you to specify the parent class, but you should
|
22
|
+
# give a string. Added for ActiveRecord belongs to compatibility.
|
23
|
+
#
|
24
|
+
# * :instance_name => How this object will appear in your views. In this case
|
25
|
+
# the default is @project. Overwrite it with a symbol.
|
26
|
+
#
|
27
|
+
# belongs_to :project, :instance_name => :my_project
|
28
|
+
#
|
29
|
+
# * :finder => Specifies which method should be called to instantiate the
|
30
|
+
# parent. Let's suppose you are using slugs ("this-is-project-title") in URLs
|
31
|
+
# so your tasks url would be: "projects/this-is-project-title/tasks". Then you
|
32
|
+
# should do this in your TasksController:
|
33
|
+
#
|
34
|
+
# belongs_to :project, :finder => :find_by_title!
|
35
|
+
#
|
36
|
+
# This will make your projects be instantiated as:
|
37
|
+
#
|
38
|
+
# Project.find_by_title!(params[:project_id])
|
39
|
+
#
|
40
|
+
# Instead of:
|
41
|
+
#
|
42
|
+
# Project.find(params[:project_id])
|
43
|
+
#
|
44
|
+
# * param => Allows you to specify params key used to instantiate the parent.
|
45
|
+
# Default is :parent_id, which in this case is :project_id.
|
46
|
+
#
|
47
|
+
# * route_name => Allows you to specify what is the route name in your url
|
48
|
+
# helper. By default is 'project'. But if your url helper should be
|
49
|
+
# "admin_project_task_url" instead of "project_task_url", just do:
|
50
|
+
#
|
51
|
+
# belongs_to :project, :route_name => "admin_project"
|
52
|
+
#
|
53
|
+
# = nested_belongs_to
|
54
|
+
#
|
55
|
+
# If for some reason you need to nested more than two resources, you can do:
|
56
|
+
#
|
57
|
+
# class TasksController
|
58
|
+
# belongs_to :company, :project
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# ATTENTION! This DOES NOT mean polymorphic associations as in resource_controller.
|
62
|
+
# Polymorphic associations are not supported yet.
|
63
|
+
#
|
64
|
+
# It means that companies have many projects which have many tasks. You URL
|
65
|
+
# should be:
|
66
|
+
#
|
67
|
+
# /companies/:company_id/projects/:project_id/tasks/:id
|
68
|
+
#
|
69
|
+
# Everything will be handled for you again. And all defaults will describe above
|
70
|
+
# will be assumed. But if you have to change the defaults. You will have to
|
71
|
+
# specify one association by one:
|
72
|
+
#
|
73
|
+
# class TasksController
|
74
|
+
# belongs_to :company, :finder => :find_by_name!, :param => :company_name
|
75
|
+
# belongs_to :project
|
76
|
+
# end
|
77
|
+
#
|
78
|
+
# belongs_to is aliased as nested_belongs_to, so this provides a nicer syntax:
|
79
|
+
#
|
80
|
+
# class TasksController
|
81
|
+
# nested_belongs_to :company, :finder => :find_by_name!, :param => :company_name
|
82
|
+
# nested_belongs_to :project
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
# In this case the association chain would be:
|
86
|
+
#
|
87
|
+
# Company.find_by_name!(params[:company_name]).projects.find(params[:project_id]).tasks.find(:all)
|
88
|
+
#
|
89
|
+
# When you are using nested resources, you have one more option to config.
|
90
|
+
# Let's suppose that to get all projects from a company, you have to do:
|
91
|
+
#
|
92
|
+
# Company.admin_projects
|
93
|
+
#
|
94
|
+
# Instead of:
|
95
|
+
#
|
96
|
+
# Company.projects
|
97
|
+
#
|
98
|
+
# In this case, you can set the collection_name in belongs_to:
|
99
|
+
#
|
100
|
+
# nested_belongs_to :project, :collection_name => 'admin_projects'
|
101
|
+
#
|
102
|
+
# = polymorphic associations
|
103
|
+
#
|
104
|
+
# In some cases you have a resource that belongs to two different resources
|
105
|
+
# but not at the same time. For example, let's suppose you have File, Message
|
106
|
+
# and Task as resources and they are all commentable.
|
107
|
+
#
|
108
|
+
# Polymorphic associations allows you to create just one controller that will
|
109
|
+
# deal with each case.
|
110
|
+
#
|
111
|
+
# class Comment < InheritedResources::Base
|
112
|
+
# belongs_to :file, :message, :task, :polymorphic => true
|
113
|
+
# end
|
114
|
+
#
|
115
|
+
# Your routes should be something like:
|
116
|
+
#
|
117
|
+
# m.resources :files, :has_many => :comments #=> /files/13/comments
|
118
|
+
# m.resources :tasks, :has_many => :comments #=> /tasks/17/comments
|
119
|
+
# m.resources :messages, :has_many => :comments #=> /messages/11/comments
|
120
|
+
#
|
121
|
+
# When using polymorphic associations, you get some free helpers:
|
122
|
+
#
|
123
|
+
# parent? #=> true
|
124
|
+
# parent_type #=> :task
|
125
|
+
# parent_class #=> Task
|
126
|
+
# parent #=> @task
|
127
|
+
#
|
128
|
+
# This polymorphic controllers thing is a great idea by James Golick and he
|
129
|
+
# built it in resource_controller. Here is just a re-implementation.
|
130
|
+
#
|
131
|
+
# = optional polymorphic associations
|
132
|
+
#
|
133
|
+
# Let's take another break from ProjectsController. Let's suppose we are
|
134
|
+
# building a store, which sell products.
|
135
|
+
#
|
136
|
+
# On the website, we can show all products, but also products scoped to
|
137
|
+
# categories, brands, users. In this case case, the association is optional, and
|
138
|
+
# we deal with it in the following way:
|
139
|
+
#
|
140
|
+
# class ProductsController < InheritedResources::Base
|
141
|
+
# belongs_to :category, :brand, :user, :polymorphic => true, :optional => true
|
142
|
+
# end
|
143
|
+
#
|
144
|
+
# This will handle all those urls properly:
|
145
|
+
#
|
146
|
+
# /products/1
|
147
|
+
# /categories/2/products/5
|
148
|
+
# /brands/10/products/3
|
149
|
+
# /user/13/products/11
|
150
|
+
#
|
151
|
+
# = nested polymorphic associations
|
152
|
+
#
|
153
|
+
# You can have polymorphic associations with nested resources. Let's suppose
|
154
|
+
# that our File, Task and Message resources in the previous example belongs to
|
155
|
+
# a project.
|
156
|
+
#
|
157
|
+
# This way we can have:
|
158
|
+
#
|
159
|
+
# class CommentsController < InheritedResources::Base
|
160
|
+
# belongs_to :project {
|
161
|
+
# belongs_to :file, :message, :task, :polymorphic => true
|
162
|
+
# }
|
163
|
+
# end
|
164
|
+
#
|
165
|
+
# Or:
|
166
|
+
#
|
167
|
+
# class CommentsController < InheritedResources::Base
|
168
|
+
# nested_belongs_to :project
|
169
|
+
# nested_belongs_to :file, :message, :task, :polymorphic => true
|
170
|
+
# end
|
171
|
+
#
|
172
|
+
# Choose the syntax that makes more sense to you. :)
|
173
|
+
#
|
174
|
+
# Finally your routes should be something like:
|
175
|
+
#
|
176
|
+
# map.resources :projects do |m|
|
177
|
+
# m.resources :files, :has_many => :comments #=> /projects/1/files/13/comments
|
178
|
+
# m.resources :tasks, :has_many => :comments #=> /projects/1/tasks/17/comments
|
179
|
+
# m.resources :messages, :has_many => :comments #=> /projects/1/messages/11/comments
|
180
|
+
# end
|
181
|
+
#
|
182
|
+
# The helpers work in the same way as above.
|
183
|
+
#
|
1
184
|
# = singleton
|
2
185
|
#
|
3
186
|
# Singletons are usually used in associations which are related through has_one
|
@@ -33,7 +216,7 @@
|
|
33
216
|
# When you have a singleton controller, the action index is removed.
|
34
217
|
#
|
35
218
|
module InheritedResources #:nodoc:
|
36
|
-
RESOURCES_CLASS_ACCESSORS = [ :resource_class, :resources_configuration, :parents_symbols, :singleton
|
219
|
+
RESOURCES_CLASS_ACCESSORS = [ :resource_class, :resources_configuration, :parents_symbols, :singleton ] unless self.const_defined? "RESOURCES_CLASS_ACCESSORS"
|
37
220
|
|
38
221
|
module ClassMethods #:nodoc:
|
39
222
|
|
@@ -94,6 +277,60 @@ module InheritedResources #:nodoc:
|
|
94
277
|
end
|
95
278
|
end
|
96
279
|
|
280
|
+
# Defines that this controller belongs to another resource.
|
281
|
+
#
|
282
|
+
# belongs_to :projects
|
283
|
+
#
|
284
|
+
def belongs_to(*symbols, &block)
|
285
|
+
options = symbols.extract_options!
|
286
|
+
|
287
|
+
options.symbolize_keys!
|
288
|
+
options.assert_valid_keys(:class_name, :parent_class, :instance_name, :param, :finder, :route_name, :collection_name, :singleton, :polymorphic, :optional)
|
289
|
+
|
290
|
+
optional = options.delete(:optional)
|
291
|
+
singleton = options.delete(:singleton)
|
292
|
+
polymorphic = options.delete(:polymorphic)
|
293
|
+
|
294
|
+
# Add BelongsToHelpers if we haven't yet.
|
295
|
+
include BelongsToHelpers if self.parents_symbols.empty?
|
296
|
+
|
297
|
+
acts_as_singleton! if singleton
|
298
|
+
acts_as_polymorphic! if polymorphic || optional
|
299
|
+
|
300
|
+
raise ArgumentError, 'You have to give me at least one association name.' if symbols.empty?
|
301
|
+
raise ArgumentError, 'You cannot define multiple associations with the options: #{options.keys.inspect}.' unless symbols.size == 1 || options.empty?
|
302
|
+
|
303
|
+
# Set configuration default values
|
304
|
+
symbols.each do |symbol|
|
305
|
+
symbol = symbol.to_sym
|
306
|
+
|
307
|
+
if polymorphic || optional
|
308
|
+
self.parents_symbols << :polymorphic unless self.parents_symbols.include? :polymorphic
|
309
|
+
self.resources_configuration[:polymorphic][:symbols] << symbol
|
310
|
+
self.resources_configuration[:polymorphic][:optional] ||= optional
|
311
|
+
else
|
312
|
+
self.parents_symbols << symbol
|
313
|
+
end
|
314
|
+
|
315
|
+
config = self.resources_configuration[symbol] = {}
|
316
|
+
config[:parent_class] = options.delete(:parent_class)
|
317
|
+
config[:parent_class] ||= (options.delete(:class_name) || symbol).to_s.classify.constantize rescue nil
|
318
|
+
config[:collection_name] = (options.delete(:collection_name) || symbol.to_s.pluralize).to_sym
|
319
|
+
config[:instance_name] = (options.delete(:instance_name) || symbol).to_sym
|
320
|
+
config[:param] = (options.delete(:param) || "#{symbol}_id").to_sym
|
321
|
+
config[:finder] = (options.delete(:finder) || :find).to_sym
|
322
|
+
config[:route_name] = (options.delete(:route_name) || symbol).to_s
|
323
|
+
end
|
324
|
+
|
325
|
+
# Regenerate url helpers unless block is given
|
326
|
+
if block_given?
|
327
|
+
class_eval(&block)
|
328
|
+
else
|
329
|
+
InheritedResources::UrlHelpers.create_resources_url_helpers!(self)
|
330
|
+
end
|
331
|
+
end
|
332
|
+
alias :nested_belongs_to :belongs_to
|
333
|
+
|
97
334
|
private
|
98
335
|
|
99
336
|
# Defines this controller as singleton.
|
@@ -111,9 +348,9 @@ module InheritedResources #:nodoc:
|
|
111
348
|
# Do not call this method on your own.
|
112
349
|
#
|
113
350
|
def acts_as_polymorphic!
|
114
|
-
|
351
|
+
unless self.parents_symbols.include? :polymorphic
|
115
352
|
include PolymorphicHelpers
|
116
|
-
helper_method :parent
|
353
|
+
helper_method :parent, :parent_type, :parent_class
|
117
354
|
end
|
118
355
|
end
|
119
356
|
|
@@ -146,7 +383,7 @@ module InheritedResources #:nodoc:
|
|
146
383
|
# Initialize polymorphic, singleton and belongs_to parameters
|
147
384
|
base.singleton = false
|
148
385
|
base.parents_symbols = []
|
149
|
-
base.
|
386
|
+
base.resources_configuration[:polymorphic] = { :symbols => [], :optional => false }
|
150
387
|
|
151
388
|
# Create helpers
|
152
389
|
InheritedResources::UrlHelpers.create_resources_url_helpers!(base)
|
@@ -8,11 +8,56 @@ module InheritedResources #:nodoc:
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def parent_class
|
11
|
-
parent.class
|
11
|
+
parent.class if @parent_type
|
12
12
|
end
|
13
13
|
|
14
14
|
def parent
|
15
|
-
instance_variable_get("@#{@parent_type}")
|
15
|
+
instance_variable_get("@#{@parent_type}") if @parent_type
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def parent?
|
21
|
+
if resources_configuration[:polymorphic][:optional]
|
22
|
+
!@parent_type.nil?
|
23
|
+
else
|
24
|
+
true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Maps parents_symbols to build association chain.
|
29
|
+
#
|
30
|
+
# If the parents_symbols find :polymorphic, it goes through the
|
31
|
+
# params keys to see which polymorphic parent matches the given params.
|
32
|
+
#
|
33
|
+
# When optional is given, it does not raise errors if the polymorphic
|
34
|
+
# params are missing.
|
35
|
+
#
|
36
|
+
def symbols_for_chain
|
37
|
+
polymorphic_config = resources_configuration[:polymorphic]
|
38
|
+
|
39
|
+
parents_symbols.map do |symbol|
|
40
|
+
if symbol == :polymorphic
|
41
|
+
params_keys = params.keys
|
42
|
+
|
43
|
+
key = polymorphic_config[:symbols].find do |poly|
|
44
|
+
params_keys.include? resources_configuration[poly][:param].to_s
|
45
|
+
end
|
46
|
+
|
47
|
+
if key.nil?
|
48
|
+
raise ScriptError, "Could not find param for polymorphic association.
|
49
|
+
The request params keys are #{params.keys.inspect}
|
50
|
+
and the polymorphic associations are
|
51
|
+
#{polymorphic_symbols.inspect}." unless polymorphic_config[:optional]
|
52
|
+
|
53
|
+
nil
|
54
|
+
else
|
55
|
+
@parent_type = key.to_sym
|
56
|
+
end
|
57
|
+
else
|
58
|
+
symbol
|
59
|
+
end
|
60
|
+
end.compact
|
16
61
|
end
|
17
62
|
|
18
63
|
end
|