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 CHANGED
@@ -1,3 +1,10 @@
1
+ # Version 0.4
2
+
3
+ * Added :optional to belongs_to associations. It allows you to deal with
4
+ categories/1/products/2 and /products/2 with just one controller.
5
+
6
+ * Cleaned up tests.
7
+
1
8
  # Version 0.3
2
9
 
3
10
  * Minor bump after three bug fixes.
data/README CHANGED
@@ -1,6 +1,6 @@
1
1
  Inherited Resources
2
2
  License: MIT
3
- Version: 0.3
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
- chain = chain.send(method_for_association_chain) if method_for_association_chain
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
- # If the parents_symbols find :polymorphic, it goes through the
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.map do |symbol|
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, :polymorphic_symbols ] unless self.const_defined? "RESOURCES_CLASS_ACCESSORS"
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
- if self.polymorphic_symbols.empty?
351
+ unless self.parents_symbols.include? :polymorphic
115
352
  include PolymorphicHelpers
116
- helper_method :parent?, :parent_type, :parent_class, :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.polymorphic_symbols = []
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