extended_inherited_resources 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,281 @@
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, :redirects)
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
+ #Add redirects configuration
50
+ self.redirects = options.delete(:redirects) || :index
51
+
52
+ if self.redirects.is_a? Hash
53
+ self.redirects[:create] ||= :index
54
+ self.redirects[:update] ||= :index
55
+ else
56
+ self.redirects = {:create => self.redirects, :update => self.redirects}
57
+ end
58
+
59
+ options.each do |key, value|
60
+ config[key] = value.to_sym
61
+ end
62
+
63
+ create_resources_url_helpers!
64
+ end
65
+
66
+ # Defines wich actions to keep from the inherited controller.
67
+ # Syntax is borrowed from resource_controller.
68
+ #
69
+ # actions :index, :show, :edit
70
+ # actions :all, :except => :index
71
+ #
72
+ def actions(*actions_to_keep)
73
+ raise ArgumentError, 'Wrong number of arguments. You have to provide which actions you want to keep.' if actions_to_keep.empty?
74
+
75
+ options = actions_to_keep.extract_options!
76
+ actions_to_remove = Array(options[:except])
77
+ actions_to_remove += ACTIONS - actions_to_keep.map { |a| a.to_sym } unless actions_to_keep.first == :all
78
+ actions_to_remove.map! { |a| a.to_sym }.uniq!
79
+ (instance_methods.map { |m| m.to_sym } & actions_to_remove).each do |action|
80
+ undef_method action, "#{action}!"
81
+ end
82
+ end
83
+
84
+ # Defines that this controller belongs to another resource.
85
+ #
86
+ # belongs_to :projects
87
+ #
88
+ # == Options
89
+ #
90
+ # * <tt>:parent_class</tt> - Allows you to specify what is the parent class.
91
+ #
92
+ # belongs_to :project, :parent_class => AdminProject
93
+ #
94
+ # * <tt>:class_name</tt> - Also allows you to specify the parent class, but you should
95
+ # give a string. Added for ActiveRecord belongs to compatibility.
96
+ #
97
+ # * <tt>:instance_name</tt> - The instance variable name. By default is the name of the association.
98
+ #
99
+ # belongs_to :project, :instance_name => :my_project
100
+ #
101
+ # * <tt>:finder</tt> - Specifies which method should be called to instantiate the parent.
102
+ #
103
+ # belongs_to :project, :finder => :find_by_title!
104
+ #
105
+ # This will make your projects be instantiated as:
106
+ #
107
+ # Project.find_by_title!(params[:project_id])
108
+ #
109
+ # Instead of:
110
+ #
111
+ # Project.find(params[:project_id])
112
+ #
113
+ # * <tt>:param</tt> - Allows you to specify params key to retrieve the id.
114
+ # Default is :association_id, which in this case is :project_id.
115
+ #
116
+ # * <tt>:route_name</tt> - Allows you to specify what is the route name in your url
117
+ # helper. By default is association name.
118
+ #
119
+ # * <tt>:collection_name</tt> - Tell how to retrieve the next collection. Let's
120
+ # suppose you have Tasks which belongs to Projects
121
+ # which belongs to companies. This will do somewhere
122
+ # down the road:
123
+ #
124
+ # @company.projects
125
+ #
126
+ # But if you want to retrieve instead:
127
+ #
128
+ # @company.admin_projects
129
+ #
130
+ # You supply the collection name.
131
+ #
132
+ # * <tt>:polymorphic</tt> - Tell the association is polymorphic.
133
+ #
134
+ # * <tt>:singleton</tt> - Tell it's a singleton association.
135
+ #
136
+ # * <tt>:optional</tt> - Tell the association is optional (it's a special
137
+ # type of polymorphic association)
138
+ #
139
+ def belongs_to(*symbols, &block)
140
+ options = symbols.extract_options!
141
+
142
+ options.symbolize_keys!
143
+ options.assert_valid_keys(:class_name, :parent_class, :instance_name, :param,
144
+ :finder, :route_name, :collection_name, :singleton,
145
+ :polymorphic, :optional)
146
+
147
+ optional = options.delete(:optional)
148
+ singleton = options.delete(:singleton)
149
+ polymorphic = options.delete(:polymorphic)
150
+ finder = options.delete(:finder)
151
+
152
+ include BelongsToHelpers if self.parents_symbols.empty?
153
+
154
+ acts_as_singleton! if singleton
155
+ acts_as_polymorphic! if polymorphic || optional
156
+
157
+ raise ArgumentError, 'You have to give me at least one association name.' if symbols.empty?
158
+ raise ArgumentError, 'You cannot define multiple associations with options: #{options.keys.inspect} to belongs to.' unless symbols.size == 1 || options.empty?
159
+
160
+ symbols.each do |symbol|
161
+ symbol = symbol.to_sym
162
+
163
+ if polymorphic || optional
164
+ self.parents_symbols << :polymorphic unless self.parents_symbols.include?(:polymorphic)
165
+ self.resources_configuration[:polymorphic][:symbols] << symbol
166
+ self.resources_configuration[:polymorphic][:optional] ||= optional
167
+ else
168
+ self.parents_symbols << symbol
169
+ end
170
+
171
+ config = self.resources_configuration[symbol] = {}
172
+
173
+ config[:parent_class] = options.delete(:parent_class) || begin
174
+ class_name = (options.delete(:class_name) || symbol).to_s.pluralize.classify
175
+ class_name.constantize
176
+ rescue NameError => e
177
+ raise unless e.message.include?(class_name)
178
+ nil
179
+ end
180
+
181
+ config[:collection_name] = options.delete(:collection_name) || symbol.to_s.pluralize.to_sym
182
+ config[:instance_name] = options.delete(:instance_name) || symbol
183
+ config[:param] = options.delete(:param) || :"#{symbol}_id"
184
+ config[:route_name] = options.delete(:route_name) || symbol
185
+ config[:finder] = finder || :find
186
+ end
187
+
188
+ if block_given?
189
+ class_eval(&block)
190
+ else
191
+ create_resources_url_helpers!
192
+ end
193
+ end
194
+
195
+ alias :nested_belongs_to :belongs_to
196
+
197
+ # A quick method to declare polymorphic belongs to.
198
+ #
199
+ def polymorphic_belongs_to(*symbols, &block)
200
+ options = symbols.extract_options!
201
+ options.merge!(:polymorphic => true)
202
+ belongs_to(*symbols << options, &block)
203
+ end
204
+
205
+ # A quick method to declare singleton belongs to.
206
+ #
207
+ def singleton_belongs_to(*symbols, &block)
208
+ options = symbols.extract_options!
209
+ options.merge!(:singleton => true)
210
+ belongs_to(*symbols << options, &block)
211
+ end
212
+
213
+ # A quick method to declare optional belongs to.
214
+ #
215
+ def optional_belongs_to(*symbols, &block)
216
+ options = symbols.extract_options!
217
+ options.merge!(:optional => true)
218
+ belongs_to(*symbols << options, &block)
219
+ end
220
+
221
+ private
222
+
223
+ def acts_as_singleton! #:nodoc:
224
+ unless self.resources_configuration[:self][:singleton]
225
+ self.resources_configuration[:self][:singleton] = true
226
+ include SingletonHelpers
227
+ actions :all, :except => :index
228
+ end
229
+ end
230
+
231
+ def acts_as_polymorphic! #:nodoc:
232
+ unless self.parents_symbols.include?(:polymorphic)
233
+ include PolymorphicHelpers
234
+ helper_method :parent, :parent_type, :parent_class, :parent?
235
+ end
236
+ end
237
+
238
+ # Initialize resources class accessors and set their default values.
239
+ #
240
+ def initialize_resources_class_accessors! #:nodoc:
241
+ # Initialize resource class
242
+ self.resource_class = begin
243
+ class_name = self.controller_name.classify
244
+ class_name.constantize
245
+ rescue NameError => e
246
+ raise unless e.message.include?(class_name)
247
+ nil
248
+ end
249
+
250
+ # Initialize resources configuration hash
251
+ self.resources_configuration ||= {}
252
+ config = self.resources_configuration[:self] = {}
253
+ config[:collection_name] = self.controller_name.to_sym
254
+ config[:instance_name] = self.controller_name.singularize.to_sym
255
+
256
+ config[:route_collection_name] = config[:collection_name]
257
+ config[:route_instance_name] = config[:instance_name]
258
+
259
+ # Deal with namespaced controllers
260
+ namespaces = self.controller_path.split('/')[0..-2]
261
+ config[:route_prefix] = namespaces.join('_') unless namespaces.empty?
262
+
263
+ self.redirects ||= {}
264
+ self.redirects[:create] ||= :index
265
+ self.redirects[:update] ||= :index
266
+
267
+ # Initialize polymorphic, singleton, scopes and belongs_to parameters
268
+ self.parents_symbols ||= []
269
+ self.resources_configuration[:polymorphic] ||= { :symbols => [], :optional => false }
270
+ end
271
+
272
+ # Hook called on inheritance.
273
+ #
274
+ def inherited(base) #:nodoc:
275
+ super(base)
276
+ base.send :initialize_resources_class_accessors!
277
+ base.send :create_resources_url_helpers!
278
+ end
279
+
280
+ end
281
+ 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,154 @@
1
+ module ActionController #:nodoc:
2
+ class Base #:nodoc:
3
+ attr_accessor :formats
4
+
5
+ class_inheritable_accessor :mimes_for_respond_to, :responder, :instance_writer => false
6
+
7
+ self.responder = ActionController::Responder
8
+ self.mimes_for_respond_to = ActiveSupport::OrderedHash.new
9
+
10
+ if defined?(ApplicationController)
11
+ ApplicationController.responder ||= ActionController::Responder
12
+ ApplicationController.mimes_for_respond_to ||= ActiveSupport::OrderedHash.new
13
+ end
14
+
15
+ # Defines mimes that are rendered by default when invoking respond_with.
16
+ #
17
+ # Examples:
18
+ #
19
+ # respond_to :html, :xml, :json
20
+ #
21
+ # All actions on your controller will respond to :html, :xml and :json.
22
+ #
23
+ # But if you want to specify it based on your actions, you can use only and
24
+ # except:
25
+ #
26
+ # respond_to :html
27
+ # respond_to :xml, :json, :except => [ :edit ]
28
+ #
29
+ # The definition above explicits that all actions respond to :html. And all
30
+ # actions except :edit respond to :xml and :json.
31
+ #
32
+ # You can specify also only parameters:
33
+ #
34
+ # respond_to :rjs, :only => :create
35
+ #
36
+ def self.respond_to(*mimes)
37
+ options = mimes.extract_options!
38
+ clear_respond_to unless mimes_for_respond_to
39
+
40
+ only_actions = Array(options.delete(:only))
41
+ except_actions = Array(options.delete(:except))
42
+
43
+ mimes.each do |mime|
44
+ mime = mime.to_sym
45
+ mimes_for_respond_to[mime] = {}
46
+ mimes_for_respond_to[mime][:only] = only_actions unless only_actions.empty?
47
+ mimes_for_respond_to[mime][:except] = except_actions unless except_actions.empty?
48
+ end
49
+ end
50
+
51
+ # Clear all mimes in respond_to.
52
+ def self.clear_respond_to
53
+ write_inheritable_attribute(:mimes_for_respond_to, ActiveSupport::OrderedHash.new)
54
+ end
55
+
56
+ def respond_to(*mimes, &block)
57
+ raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?
58
+ if response = retrieve_response_from_mimes(mimes, &block)
59
+ response.call
60
+ end
61
+ end
62
+
63
+ def respond_with(*resources, &block)
64
+ if response = retrieve_response_from_mimes([], &block)
65
+ options = resources.extract_options!
66
+ options.merge!(:default_response => response)
67
+ (options.delete(:responder) || responder).call(self, resources, options)
68
+ end
69
+ end
70
+
71
+ protected
72
+
73
+ # Collect mimes declared in the class method respond_to valid for the
74
+ # current action.
75
+ #
76
+ def collect_mimes_from_class_level #:nodoc:
77
+ action = action_name.to_sym
78
+
79
+ mimes_for_respond_to.keys.select do |mime|
80
+ config = mimes_for_respond_to[mime]
81
+
82
+ if config[:except]
83
+ !config[:except].include?(action)
84
+ elsif config[:only]
85
+ config[:only].include?(action)
86
+ else
87
+ true
88
+ end
89
+ end
90
+ end
91
+
92
+ # Collects mimes and return the response for the negotiated format. Returns
93
+ # nil if :not_acceptable was sent to the client.
94
+ #
95
+ def retrieve_response_from_mimes(mimes, &block)
96
+ responder = ActionController::MimeResponds::Responder.new(self)
97
+ mimes = collect_mimes_from_class_level if mimes.empty?
98
+ mimes.each { |mime| responder.send(mime) }
99
+ block.call(responder) if block_given?
100
+
101
+ if format = responder.negotiate_mime
102
+ self.response.template.template_format = format.to_sym
103
+ self.response.content_type = format.to_s
104
+ self.formats = [ format.to_sym ]
105
+ responder.response_for(format) || proc { default_render }
106
+ else
107
+ head :not_acceptable
108
+ nil
109
+ end
110
+ end
111
+ end
112
+
113
+ module MimeResponds
114
+ class Responder #:nodoc:
115
+ attr_reader :order
116
+
117
+ def any(*args, &block)
118
+ if args.any?
119
+ args.each { |type| send(type, &block) }
120
+ else
121
+ custom(Mime::ALL, &block)
122
+ end
123
+ end
124
+ alias :all :any
125
+
126
+ def custom(mime_type, &block)
127
+ mime_type = mime_type.is_a?(Mime::Type) ? mime_type : Mime::Type.lookup(mime_type.to_s)
128
+ @order << mime_type
129
+ @responses[mime_type] ||= block
130
+ end
131
+
132
+ def response_for(mime)
133
+ @responses[mime] || @responses[Mime::ALL]
134
+ end
135
+
136
+ def negotiate_mime
137
+ @mime_type_priority.each do |priority|
138
+ if priority == Mime::ALL
139
+ return @order.first
140
+ elsif @order.include?(priority)
141
+ return priority
142
+ end
143
+ end
144
+
145
+ if @order.include?(Mime::ALL)
146
+ return Mime::SET.first if @mime_type_priority.first == Mime::ALL
147
+ return @mime_type_priority.first
148
+ end
149
+
150
+ nil
151
+ end
152
+ end
153
+ end
154
+ end