karsthammer-inherited_resources 1.1.2

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.
Files changed (36) hide show
  1. data/CHANGELOG +119 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +516 -0
  4. data/Rakefile +43 -0
  5. data/lib/generators/rails/USAGE +10 -0
  6. data/lib/generators/rails/inherited_resources_controller_generator.rb +11 -0
  7. data/lib/generators/rails/templates/controller.rb +5 -0
  8. data/lib/inherited_resources.rb +37 -0
  9. data/lib/inherited_resources/actions.rb +67 -0
  10. data/lib/inherited_resources/base.rb +44 -0
  11. data/lib/inherited_resources/base_helpers.rb +270 -0
  12. data/lib/inherited_resources/belongs_to_helpers.rb +97 -0
  13. data/lib/inherited_resources/blank_slate.rb +12 -0
  14. data/lib/inherited_resources/class_methods.rb +267 -0
  15. data/lib/inherited_resources/dsl.rb +26 -0
  16. data/lib/inherited_resources/polymorphic_helpers.rb +155 -0
  17. data/lib/inherited_resources/responder.rb +6 -0
  18. data/lib/inherited_resources/singleton_helpers.rb +95 -0
  19. data/lib/inherited_resources/url_helpers.rb +188 -0
  20. data/lib/inherited_resources/version.rb +3 -0
  21. data/test/aliases_test.rb +144 -0
  22. data/test/association_chain_test.rb +125 -0
  23. data/test/base_test.rb +278 -0
  24. data/test/belongs_to_test.rb +105 -0
  25. data/test/class_methods_test.rb +132 -0
  26. data/test/customized_base_test.rb +168 -0
  27. data/test/customized_belongs_to_test.rb +76 -0
  28. data/test/defaults_test.rb +70 -0
  29. data/test/nested_belongs_to_test.rb +108 -0
  30. data/test/optional_belongs_to_test.rb +164 -0
  31. data/test/polymorphic_test.rb +186 -0
  32. data/test/redirect_to_test.rb +51 -0
  33. data/test/singleton_test.rb +83 -0
  34. data/test/test_helper.rb +40 -0
  35. data/test/url_helpers_test.rb +665 -0
  36. metadata +142 -0
@@ -0,0 +1,97 @@
1
+ module InheritedResources
2
+
3
+ # = belongs_to
4
+ #
5
+ # Let's suppose that we have some tasks that belongs to projects. To specify
6
+ # this assoication in your controllers, just do:
7
+ #
8
+ # class TasksController < InheritedResources::Base
9
+ # belongs_to :project
10
+ # end
11
+ #
12
+ # belongs_to accepts several options to be able to configure the association.
13
+ # For example, if you want urls like /projects/:project_title/tasks, you
14
+ # can customize how InheritedResources find your projects:
15
+ #
16
+ # class TasksController < InheritedResources::Base
17
+ # belongs_to :project, :finder => :find_by_title!, :param => :project_title
18
+ # end
19
+ #
20
+ # It also accepts :route_name, :parent_class and :instance_name as options.
21
+ # Check the lib/inherited_resources/class_methods.rb for more.
22
+ #
23
+ # = nested_belongs_to
24
+ #
25
+ # Now, our Tasks get some Comments and you need to nest even deeper. Good
26
+ # practices says that you should never nest more than two resources, but sometimes
27
+ # you have to for security reasons. So this is an example of how you can do it:
28
+ #
29
+ # class CommentsController < InheritedResources::Base
30
+ # nested_belongs_to :project, :task
31
+ # end
32
+ #
33
+ # If you need to configure any of these belongs to, you can nested them using blocks:
34
+ #
35
+ # class CommentsController < InheritedResources::Base
36
+ # belongs_to :project, :finder => :find_by_title!, :param => :project_title do
37
+ # belongs_to :task
38
+ # end
39
+ # end
40
+ #
41
+ # Warning: calling several belongs_to is the same as nesting them:
42
+ #
43
+ # class CommentsController < InheritedResources::Base
44
+ # belongs_to :project
45
+ # belongs_to :task
46
+ # end
47
+ #
48
+ # In other words, the code above is the same as calling nested_belongs_to.
49
+ #
50
+ module BelongsToHelpers
51
+
52
+ protected
53
+
54
+ # Parent is always true when belongs_to is called.
55
+ #
56
+ def parent?
57
+ true
58
+ end
59
+
60
+ def parent
61
+ @parent ||= association_chain[-1]
62
+ end
63
+
64
+ def parent_type
65
+ parent.class.name.underscore.to_sym
66
+ end
67
+
68
+ private
69
+
70
+ # Evaluate the parent given. This is used to nest parents in the
71
+ # association chain.
72
+ #
73
+ def evaluate_parent(parent_symbol, parent_config, chain = nil) #:nodoc:
74
+ instantiated_object = instance_variable_get("@#{parent_config[:instance_name]}")
75
+ return instantiated_object if instantiated_object
76
+
77
+ parent = if chain
78
+ chain.send(parent_config[:collection_name])
79
+ else
80
+ parent_config[:parent_class]
81
+ end
82
+
83
+ parent = parent.send(parent_config[:finder], params[parent_config[:param]])
84
+
85
+ instance_variable_set("@#{parent_config[:instance_name]}", parent)
86
+ end
87
+
88
+ # Maps parents_symbols to build association chain. In this case, it
89
+ # simply return the parent_symbols, however on polymorphic belongs to,
90
+ # it has some customization.
91
+ #
92
+ def symbols_for_association_chain #:nodoc:
93
+ parents_symbols
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,12 @@
1
+ module InheritedResources
2
+ # An object from BlankSlate simply discards all messages sent to it.
3
+ class BlankSlate
4
+ instance_methods.each do |m|
5
+ undef_method m unless m =~ /^(__|object_id)/
6
+ end
7
+
8
+ def method_missing(*args)
9
+ nil
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,267 @@
1
+ module InheritedResources
2
+ module ClassMethods
3
+
4
+ protected
5
+
6
+ # Used to overwrite the default assumptions InheritedResources do. Whenever
7
+ # this method is called, it should be on the top of your controller, since
8
+ # almost other methods depends on the values given to <<tt>>defaults</tt>.
9
+ #
10
+ # == Options
11
+ #
12
+ # * <tt>:resource_class</tt> - The resource class which by default is guessed
13
+ # by the controller name. Defaults to Project in
14
+ # ProjectsController.
15
+ #
16
+ # * <tt>:collection_name</tt> - The name of the collection instance variable which
17
+ # is set on the index action. Defaults to :projects in
18
+ # ProjectsController.
19
+ #
20
+ # * <tt>:instance_name</tt> - The name of the singular instance variable which
21
+ # is set on all actions besides index action. Defaults to
22
+ # :project in ProjectsController.
23
+ #
24
+ # * <tt>:route_collection_name</tt> - The name of the collection route. Defaults to :collection_name.
25
+ #
26
+ # * <tt>:route_instance_name</tt> - The name of the singular route. Defaults to :instance_name.
27
+ #
28
+ # * <tt>:route_prefix</tt> - The route prefix which is automically set in namespaced
29
+ # controllers. Default to :admin on Admin::ProjectsController.
30
+ #
31
+ # * <tt>:singleton</tt> - Tells if this controller is singleton or not.
32
+ #
33
+ def defaults(options)
34
+ raise ArgumentError, 'Class method :defaults expects a hash of options.' unless options.is_a? Hash
35
+
36
+ options.symbolize_keys!
37
+ options.assert_valid_keys(:resource_class, :collection_name, :instance_name,
38
+ :class_name, :route_prefix, :route_collection_name,
39
+ :route_instance_name, :singleton)
40
+
41
+ self.resource_class = options.delete(:resource_class) if options.key?(:resource_class)
42
+ self.resource_class = options.delete(:class_name).constantize if options.key?(:class_name)
43
+
44
+ acts_as_singleton! if options.delete(:singleton)
45
+
46
+ config = self.resources_configuration[:self]
47
+ config[:route_prefix] = options.delete(:route_prefix) if options.key?(:route_prefix)
48
+
49
+ options.each do |key, value|
50
+ config[key] = value.to_sym
51
+ end
52
+
53
+ create_resources_url_helpers!
54
+ end
55
+
56
+ # Defines wich actions to keep from the inherited controller.
57
+ # Syntax is borrowed from resource_controller.
58
+ #
59
+ # actions :index, :show, :edit
60
+ # actions :all, :except => :index
61
+ #
62
+ def actions(*actions_to_keep)
63
+ raise ArgumentError, 'Wrong number of arguments. You have to provide which actions you want to keep.' if actions_to_keep.empty?
64
+
65
+ options = actions_to_keep.extract_options!
66
+ actions_to_remove = Array(options[:except])
67
+ actions_to_remove += ACTIONS - actions_to_keep.map { |a| a.to_sym } unless actions_to_keep.first == :all
68
+ actions_to_remove.map! { |a| a.to_sym }.uniq!
69
+ (instance_methods.map { |m| m.to_sym } & actions_to_remove).each do |action|
70
+ undef_method action, "#{action}!"
71
+ end
72
+ end
73
+
74
+ # Defines that this controller belongs to another resource.
75
+ #
76
+ # belongs_to :projects
77
+ #
78
+ # == Options
79
+ #
80
+ # * <tt>:parent_class</tt> - Allows you to specify what is the parent class.
81
+ #
82
+ # belongs_to :project, :parent_class => AdminProject
83
+ #
84
+ # * <tt>:class_name</tt> - Also allows you to specify the parent class, but you should
85
+ # give a string. Added for ActiveRecord belongs to compatibility.
86
+ #
87
+ # * <tt>:instance_name</tt> - The instance variable name. By default is the name of the association.
88
+ #
89
+ # belongs_to :project, :instance_name => :my_project
90
+ #
91
+ # * <tt>:finder</tt> - Specifies which method should be called to instantiate the parent.
92
+ #
93
+ # belongs_to :project, :finder => :find_by_title!
94
+ #
95
+ # This will make your projects be instantiated as:
96
+ #
97
+ # Project.find_by_title!(params[:project_id])
98
+ #
99
+ # Instead of:
100
+ #
101
+ # Project.find(params[:project_id])
102
+ #
103
+ # * <tt>:param</tt> - Allows you to specify params key to retrieve the id.
104
+ # Default is :association_id, which in this case is :project_id.
105
+ #
106
+ # * <tt>:route_name</tt> - Allows you to specify what is the route name in your url
107
+ # helper. By default is association name.
108
+ #
109
+ # * <tt>:collection_name</tt> - Tell how to retrieve the next collection. Let's
110
+ # suppose you have Tasks which belongs to Projects
111
+ # which belongs to companies. This will do somewhere
112
+ # down the road:
113
+ #
114
+ # @company.projects
115
+ #
116
+ # But if you want to retrieve instead:
117
+ #
118
+ # @company.admin_projects
119
+ #
120
+ # You supply the collection name.
121
+ #
122
+ # * <tt>:polymorphic</tt> - Tell the association is polymorphic.
123
+ #
124
+ # * <tt>:singleton</tt> - Tell it's a singleton association.
125
+ #
126
+ # * <tt>:optional</tt> - Tell the association is optional (it's a special
127
+ # type of polymorphic association)
128
+ #
129
+ def belongs_to(*symbols, &block)
130
+ options = symbols.extract_options!
131
+
132
+ options.symbolize_keys!
133
+ options.assert_valid_keys(:class_name, :parent_class, :instance_name, :param,
134
+ :finder, :route_name, :collection_name, :singleton,
135
+ :polymorphic, :optional)
136
+
137
+ optional = options.delete(:optional)
138
+ singleton = options.delete(:singleton)
139
+ polymorphic = options.delete(:polymorphic)
140
+ finder = options.delete(:finder)
141
+
142
+ include BelongsToHelpers if self.parents_symbols.empty?
143
+
144
+ acts_as_singleton! if singleton
145
+ acts_as_polymorphic! if polymorphic || optional
146
+
147
+ raise ArgumentError, 'You have to give me at least one association name.' if symbols.empty?
148
+ raise ArgumentError, 'You cannot define multiple associations with options: #{options.keys.inspect} to belongs to.' unless symbols.size == 1 || options.empty?
149
+
150
+ symbols.each do |symbol|
151
+ symbol = symbol.to_sym
152
+
153
+ if polymorphic || optional
154
+ self.parents_symbols << :polymorphic unless self.parents_symbols.include?(:polymorphic)
155
+ self.resources_configuration[:polymorphic][:symbols] << symbol
156
+ self.resources_configuration[:polymorphic][:optional] ||= optional
157
+ else
158
+ self.parents_symbols << symbol
159
+ end
160
+
161
+ config = self.resources_configuration[symbol] = {}
162
+
163
+ config[:parent_class] = options.delete(:parent_class) || begin
164
+ class_name = (options.delete(:class_name) || symbol).to_s.pluralize.classify
165
+ class_name.constantize
166
+ rescue NameError => e
167
+ raise unless e.message.include?(class_name)
168
+ nil
169
+ end
170
+
171
+ config[:collection_name] = options.delete(:collection_name) || symbol.to_s.pluralize.to_sym
172
+ config[:instance_name] = options.delete(:instance_name) || symbol
173
+ config[:param] = options.delete(:param) || :"#{symbol}_id"
174
+ config[:route_name] = options.delete(:route_name) || symbol
175
+ config[:finder] = finder || :find
176
+ end
177
+
178
+ if block_given?
179
+ class_eval(&block)
180
+ else
181
+ create_resources_url_helpers!
182
+ end
183
+ helper_method :parent, :parent?
184
+ end
185
+ alias :nested_belongs_to :belongs_to
186
+
187
+ # A quick method to declare polymorphic belongs to.
188
+ #
189
+ def polymorphic_belongs_to(*symbols, &block)
190
+ options = symbols.extract_options!
191
+ options.merge!(:polymorphic => true)
192
+ belongs_to(*symbols << options, &block)
193
+ end
194
+
195
+ # A quick method to declare singleton belongs to.
196
+ #
197
+ def singleton_belongs_to(*symbols, &block)
198
+ options = symbols.extract_options!
199
+ options.merge!(:singleton => true)
200
+ belongs_to(*symbols << options, &block)
201
+ end
202
+
203
+ # A quick method to declare optional belongs to.
204
+ #
205
+ def optional_belongs_to(*symbols, &block)
206
+ options = symbols.extract_options!
207
+ options.merge!(:optional => true)
208
+ belongs_to(*symbols << options, &block)
209
+ end
210
+
211
+ private
212
+
213
+ def acts_as_singleton! #:nodoc:
214
+ unless self.resources_configuration[:self][:singleton]
215
+ self.resources_configuration[:self][:singleton] = true
216
+ include SingletonHelpers
217
+ actions :all, :except => :index
218
+ end
219
+ end
220
+
221
+ def acts_as_polymorphic! #:nodoc:
222
+ unless self.parents_symbols.include?(:polymorphic)
223
+ include PolymorphicHelpers
224
+ helper_method :parent_type, :parent_class
225
+ end
226
+ end
227
+
228
+ # Initialize resources class accessors and set their default values.
229
+ #
230
+ def initialize_resources_class_accessors! #:nodoc:
231
+ # Initialize resource class
232
+ self.resource_class = begin
233
+ class_name = self.controller_name.classify
234
+ class_name.constantize
235
+ rescue NameError => e
236
+ raise unless e.message.include?(class_name)
237
+ nil
238
+ end
239
+
240
+ # Initialize resources configuration hash
241
+ self.resources_configuration ||= {}
242
+ config = self.resources_configuration[:self] = {}
243
+ config[:collection_name] = self.controller_name.to_sym
244
+ config[:instance_name] = self.controller_name.singularize.to_sym
245
+
246
+ config[:route_collection_name] = config[:collection_name]
247
+ config[:route_instance_name] = config[:instance_name]
248
+
249
+ # Deal with namespaced controllers
250
+ namespaces = self.controller_path.split('/')[0..-2]
251
+ config[:route_prefix] = namespaces.join('_') unless namespaces.empty?
252
+
253
+ # Initialize polymorphic, singleton, scopes and belongs_to parameters
254
+ self.parents_symbols ||= []
255
+ self.resources_configuration[:polymorphic] ||= { :symbols => [], :optional => false }
256
+ end
257
+
258
+ # Hook called on inheritance.
259
+ #
260
+ def inherited(base) #:nodoc:
261
+ super(base)
262
+ base.send :initialize_resources_class_accessors!
263
+ base.send :create_resources_url_helpers!
264
+ end
265
+
266
+ end
267
+ end
@@ -0,0 +1,26 @@
1
+ module InheritedResources
2
+ # Allows controllers to write actions using a class method DSL.
3
+ #
4
+ # class MyController < InheritedResources::Base
5
+ # create! do |success, failure|
6
+ # success.html { render :text => "It works!" }
7
+ # end
8
+ # end
9
+ #
10
+ module DSL
11
+ def self.included(base)
12
+ ACTIONS.each do |action|
13
+ base.class_eval <<-WRITTER
14
+ def self.#{action}!(options={}, &block)
15
+ define_method :__#{action}, &block
16
+ class_eval <<-ACTION
17
+ def #{action}
18
+ super(\#{options.inspect}, &method(:__#{action}))
19
+ end
20
+ ACTION
21
+ end
22
+ WRITTER
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,155 @@
1
+ module InheritedResources
2
+
3
+ # = polymorphic associations
4
+ #
5
+ # In some cases you have a resource that belongs to two different resources
6
+ # but not at the same time. For example, let's suppose you have File, Message
7
+ # and Task as resources and they are all commentable.
8
+ #
9
+ # Polymorphic associations allows you to create just one controller that will
10
+ # deal with each case.
11
+ #
12
+ # class Comment < InheritedResources::Base
13
+ # belongs_to :file, :message, :task, :polymorphic => true
14
+ # end
15
+ #
16
+ # Your routes should be something like:
17
+ #
18
+ # m.resources :files, :has_many => :comments #=> /files/13/comments
19
+ # m.resources :tasks, :has_many => :comments #=> /tasks/17/comments
20
+ # m.resources :messages, :has_many => :comments #=> /messages/11/comments
21
+ #
22
+ # When using polymorphic associations, you get some free helpers:
23
+ #
24
+ # parent? #=> true
25
+ # parent_type #=> :task
26
+ # parent_class #=> Task
27
+ # parent #=> @task
28
+ #
29
+ # This polymorphic controllers thing is a great idea by James Golick and he
30
+ # built it in resource_controller. Here is just a re-implementation.
31
+ #
32
+ # = optional polymorphic associations
33
+ #
34
+ # Let's take another break from ProjectsController. Let's suppose we are
35
+ # building a store, which sell products.
36
+ #
37
+ # On the website, we can show all products, but also products scoped to
38
+ # categories, brands, users. In this case case, the association is optional, and
39
+ # we deal with it in the following way:
40
+ #
41
+ # class ProductsController < InheritedResources::Base
42
+ # belongs_to :category, :brand, :user, :polymorphic => true, :optional => true
43
+ # end
44
+ #
45
+ # This will handle all those urls properly:
46
+ #
47
+ # /products/1
48
+ # /categories/2/products/5
49
+ # /brands/10/products/3
50
+ # /user/13/products/11
51
+ #
52
+ # = nested polymorphic associations
53
+ #
54
+ # You can have polymorphic associations with nested resources. Let's suppose
55
+ # that our File, Task and Message resources in the previous example belongs to
56
+ # a project.
57
+ #
58
+ # This way we can have:
59
+ #
60
+ # class CommentsController < InheritedResources::Base
61
+ # belongs_to :project {
62
+ # belongs_to :file, :message, :task, :polymorphic => true
63
+ # }
64
+ # end
65
+ #
66
+ # Or:
67
+ #
68
+ # class CommentsController < InheritedResources::Base
69
+ # nested_belongs_to :project
70
+ # nested_belongs_to :file, :message, :task, :polymorphic => true
71
+ # end
72
+ #
73
+ # Choose the syntax that makes more sense to you. :)
74
+ #
75
+ # Finally your routes should be something like:
76
+ #
77
+ # map.resources :projects do |m|
78
+ # m.resources :files, :has_many => :comments #=> /projects/1/files/13/comments
79
+ # m.resources :tasks, :has_many => :comments #=> /projects/1/tasks/17/comments
80
+ # m.resources :messages, :has_many => :comments #=> /projects/1/messages/11/comments
81
+ # end
82
+ #
83
+ # The helpers work in the same way as above.
84
+ #
85
+ module PolymorphicHelpers
86
+
87
+ protected
88
+
89
+ # Returns the parent type. A Comments class can have :task, :file, :note
90
+ # as parent types.
91
+ #
92
+ def parent_type
93
+ @parent_type
94
+ end
95
+
96
+ def parent_class
97
+ parent.class if @parent_type
98
+ end
99
+
100
+ # Returns the parent object. They are also available with the instance
101
+ # variable name: @task, @file, @note...
102
+ #
103
+ def parent
104
+ instance_variable_get("@#{@parent_type}") if @parent_type
105
+ end
106
+
107
+ # If the polymorphic association is optional, we might not have a parent.
108
+ #
109
+ def parent?
110
+ if resources_configuration[:polymorphic][:optional]
111
+ parents_symbols.size > 1 || !@parent_type.nil?
112
+ else
113
+ true
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ # Maps parents_symbols to build association chain.
120
+ #
121
+ # If the parents_symbols find :polymorphic, it goes through the
122
+ # params keys to see which polymorphic parent matches the given params.
123
+ #
124
+ # When optional is given, it does not raise errors if the polymorphic
125
+ # params are missing.
126
+ #
127
+ def symbols_for_association_chain #:nodoc:
128
+ polymorphic_config = resources_configuration[:polymorphic]
129
+ parents_symbols.map do |symbol|
130
+ if symbol == :polymorphic
131
+ params_keys = params.keys
132
+
133
+ keys = polymorphic_config[:symbols].map do |poly|
134
+ params_keys.include?(resources_configuration[poly][:param].to_s) ? poly : nil
135
+ end.compact
136
+
137
+ if keys.empty?
138
+ raise ScriptError, "Could not find param for polymorphic association. The request" <<
139
+ "parameters are #{params.keys.inspect} and the polymorphic " <<
140
+ "associations are #{polymorphic_config[:symbols].inspect}." unless polymorphic_config[:optional]
141
+
142
+ nil
143
+ else
144
+ @parent_type = keys[-1].to_sym
145
+ @parent_types = keys.map(&:to_sym)
146
+ end
147
+ else
148
+ symbol
149
+ end
150
+ end.flatten.compact
151
+ end
152
+
153
+ end
154
+ end
155
+