josevalim-inherited_resources 0.3 → 0.4

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 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