emmanuel-inherited_resources 0.9.1

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.
@@ -0,0 +1,334 @@
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_keep.map!{ |a| a.to_s }
67
+
68
+ actions_to_remove = Array(options[:except])
69
+ actions_to_remove.map!{ |a| a.to_s }
70
+
71
+ actions_to_remove += ACTIONS.map{ |a| a.to_s } - actions_to_keep unless actions_to_keep.first == 'all'
72
+ actions_to_remove.uniq!
73
+
74
+ (instance_methods & actions_to_remove).each do |action|
75
+ undef_method action, "#{action}!"
76
+ end
77
+ end
78
+
79
+ # Detects params from url and apply as scopes to your classes.
80
+ #
81
+ # Your model:
82
+ #
83
+ # class Graduation < ActiveRecord::Base
84
+ # named_scope :featured, :conditions => { :featured => true }
85
+ # named_scope :by_degree, proc {|degree| { :conditions => { :degree => degree } } }
86
+ # end
87
+ #
88
+ # Your controller:
89
+ #
90
+ # class GraduationsController < InheritedResources::Base
91
+ # has_scope :featured, :boolean => true, :only => :index
92
+ # has_scope :by_degree, :only => :index
93
+ # end
94
+ #
95
+ # Then for each request:
96
+ #
97
+ # /graduations
98
+ # #=> acts like a normal request
99
+ #
100
+ # /graduations?featured=true
101
+ # #=> calls the named scope and bring featured graduations
102
+ #
103
+ # /graduations?featured=true&by_degree=phd
104
+ # #=> brings featured graduations with phd degree
105
+ #
106
+ # You can retrieve the current scopes in use with <tt>current_scopes</tt>
107
+ # method. In the last case, it would return: { :featured => "true", :by_degree => "phd" }
108
+ #
109
+ # == Options
110
+ #
111
+ # * <tt>:boolean</tt> - When set to true, call the scope only when the param is true or 1,
112
+ # and does not send the value as argument.
113
+ #
114
+ # * <tt>:only</tt> - In which actions the scope is applied. By default is :all.
115
+ #
116
+ # * <tt>:except</tt> - In which actions the scope is not applied. By default is :none.
117
+ #
118
+ # * <tt>:as</tt> - The key in the params hash expected to find the scope.
119
+ # Defaults to the scope name.
120
+ #
121
+ # * <tt>:default</tt> - Default value for the scope. Whenever supplied the scope
122
+ # is always called. This is useful to add easy pagination.
123
+ #
124
+ def has_scope(*scopes)
125
+ options = scopes.extract_options!
126
+
127
+ options.symbolize_keys!
128
+ options.assert_valid_keys(:boolean, :key, :only, :except, :default, :as)
129
+
130
+ if options[:key]
131
+ ActiveSupport::Deprecation.warn "has_scope :key is deprecated, use :as instead"
132
+ options[:as] ||= options[:key]
133
+ end
134
+
135
+ if self.scopes_configuration.empty?
136
+ include HasScopeHelpers
137
+ helper_method :current_scopes
138
+ end
139
+
140
+ scopes.each do |scope|
141
+ self.scopes_configuration[scope] ||= {}
142
+ self.scopes_configuration[scope][:as] = options[:as] || scope
143
+ self.scopes_configuration[scope][:only] = Array(options[:only])
144
+ self.scopes_configuration[scope][:except] = Array(options[:except])
145
+ self.scopes_configuration[scope][:boolean] = options[:boolean] if options.key?(:boolean)
146
+ self.scopes_configuration[scope][:default] = options[:default] if options.key?(:default)
147
+ end
148
+ end
149
+
150
+ # Defines that this controller belongs to another resource.
151
+ #
152
+ # belongs_to :projects
153
+ #
154
+ # == Options
155
+ #
156
+ # * <tt>:parent_class</tt> - Allows you to specify what is the parent class.
157
+ #
158
+ # belongs_to :project, :parent_class => AdminProject
159
+ #
160
+ # * <tt>:class_name</tt> - Also allows you to specify the parent class, but you should
161
+ # give a string. Added for ActiveRecord belongs to compatibility.
162
+ #
163
+ # * <tt>:instance_name</tt> - The instance variable name. By default is the name of the association.
164
+ #
165
+ # belongs_to :project, :instance_name => :my_project
166
+ #
167
+ # * <tt>:finder</tt> - Specifies which method should be called to instantiate the parent.
168
+ #
169
+ # belongs_to :project, :finder => :find_by_title!
170
+ #
171
+ # This will make your projects be instantiated as:
172
+ #
173
+ # Project.find_by_title!(params[:project_id])
174
+ #
175
+ # Instead of:
176
+ #
177
+ # Project.find(params[:project_id])
178
+ #
179
+ # * <tt>:param</tt> - Allows you to specify params key to retrieve the id.
180
+ # Default is :association_id, which in this case is :project_id.
181
+ #
182
+ # * <tt>:route_name</tt> - Allows you to specify what is the route name in your url
183
+ # helper. By default is association name.
184
+ #
185
+ # * <tt>:collection_name</tt> - Tell how to retrieve the next collection. Let's
186
+ # suppose you have Tasks which belongs to Projects
187
+ # which belongs to companies. This will do somewhere
188
+ # down the road:
189
+ #
190
+ # @company.projects
191
+ #
192
+ # But if you want to retrieve instead:
193
+ #
194
+ # @company.admin_projects
195
+ #
196
+ # You supply the collection name.
197
+ #
198
+ # * <tt>:polymorphic</tt> - Tell the association is polymorphic.
199
+ #
200
+ # * <tt>:singleton</tt> - Tell it's a singleton association.
201
+ #
202
+ # * <tt>:optional</tt> - Tell the association is optional (it's a special
203
+ # type of polymorphic association)
204
+ #
205
+ def belongs_to(*symbols, &block)
206
+ options = symbols.extract_options!
207
+
208
+ options.symbolize_keys!
209
+ options.assert_valid_keys(:class_name, :parent_class, :instance_name, :param,
210
+ :finder, :route_name, :collection_name, :singleton,
211
+ :polymorphic, :optional)
212
+
213
+ optional = options.delete(:optional)
214
+ singleton = options.delete(:singleton)
215
+ polymorphic = options.delete(:polymorphic)
216
+ finder = options.delete(:finder)
217
+
218
+ include BelongsToHelpers if self.parents_symbols.empty?
219
+
220
+ acts_as_singleton! if singleton
221
+ acts_as_polymorphic! if polymorphic || optional
222
+
223
+ raise ArgumentError, 'You have to give me at least one association name.' if symbols.empty?
224
+ raise ArgumentError, 'You cannot define multiple associations with options: #{options.keys.inspect} to belongs to.' unless symbols.size == 1 || options.empty?
225
+
226
+ symbols.each do |symbol|
227
+ symbol = symbol.to_sym
228
+
229
+ if polymorphic || optional
230
+ self.parents_symbols << :polymorphic unless self.parents_symbols.include?(:polymorphic)
231
+ self.resources_configuration[:polymorphic][:symbols] << symbol
232
+ self.resources_configuration[:polymorphic][:optional] ||= optional
233
+ else
234
+ self.parents_symbols << symbol
235
+ end
236
+
237
+ config = self.resources_configuration[symbol] = {}
238
+ config[:parent_class] = options.delete(:parent_class)
239
+ config[:parent_class] ||= (options.delete(:class_name) || symbol).to_s.classify.constantize rescue nil
240
+ config[:collection_name] = options.delete(:collection_name) || symbol.to_s.pluralize.to_sym
241
+ config[:instance_name] = options.delete(:instance_name) || symbol
242
+ config[:param] = options.delete(:param) || :"#{symbol}_id"
243
+ config[:route_name] = options.delete(:route_name) || symbol
244
+ config[:finder] = finder || :find
245
+ end
246
+
247
+ if block_given?
248
+ class_eval(&block)
249
+ else
250
+ create_resources_url_helpers!
251
+ end
252
+ end
253
+ alias :nested_belongs_to :belongs_to
254
+
255
+ # A quick method to declare polymorphic belongs to.
256
+ #
257
+ def polymorphic_belongs_to(*symbols, &block)
258
+ options = symbols.extract_options!
259
+ options.merge!(:polymorphic => true)
260
+ belongs_to(*symbols << options, &block)
261
+ end
262
+
263
+ # A quick method to declare singleton belongs to.
264
+ #
265
+ def singleton_belongs_to(*symbols, &block)
266
+ options = symbols.extract_options!
267
+ options.merge!(:singleton => true)
268
+ belongs_to(*symbols << options, &block)
269
+ end
270
+
271
+ # A quick method to declare optional belongs to.
272
+ #
273
+ def optional_belongs_to(*symbols, &block)
274
+ options = symbols.extract_options!
275
+ options.merge!(:optional => true)
276
+ belongs_to(*symbols << options, &block)
277
+ end
278
+
279
+ private
280
+
281
+ def acts_as_singleton! #:nodoc:
282
+ unless self.resources_configuration[:self][:singleton]
283
+ self.resources_configuration[:self][:singleton] = true
284
+ include SingletonHelpers
285
+ actions :all, :except => :index
286
+ end
287
+ end
288
+
289
+ def acts_as_polymorphic! #:nodoc:
290
+ unless self.parents_symbols.include?(:polymorphic)
291
+ include PolymorphicHelpers
292
+ helper_method :parent, :parent_type, :parent_class, :parent?
293
+ end
294
+ end
295
+
296
+ # Initialize resources class accessors and set their default values.
297
+ #
298
+ def initialize_resources_class_accessors! #:nodoc:
299
+ # Initialize resource class
300
+ self.resource_class = begin
301
+ self.controller_name.classify.constantize
302
+ rescue NameError
303
+ nil
304
+ end
305
+
306
+ # Initialize resources configuration hash
307
+ self.resources_configuration ||= {}
308
+ config = self.resources_configuration[:self] = {}
309
+ config[:collection_name] = self.controller_name.to_sym
310
+ config[:instance_name] = self.controller_name.singularize.to_sym
311
+
312
+ config[:route_collection_name] = config[:collection_name]
313
+ config[:route_instance_name] = config[:instance_name]
314
+
315
+ # Deal with namespaced controllers
316
+ namespaces = self.controller_path.split('/')[0..-2]
317
+ config[:route_prefix] = namespaces.join('_') unless namespaces.empty?
318
+
319
+ # Initialize polymorphic, singleton, scopes and belongs_to parameters
320
+ self.parents_symbols ||= []
321
+ self.scopes_configuration ||= {}
322
+ self.resources_configuration[:polymorphic] ||= { :symbols => [], :optional => false }
323
+ end
324
+
325
+ # Hook called on inheritance.
326
+ #
327
+ def inherited(base) #:nodoc:
328
+ super(base)
329
+ base.send :initialize_resources_class_accessors!
330
+ base.send :create_resources_url_helpers!
331
+ end
332
+
333
+ end
334
+ end
@@ -0,0 +1,20 @@
1
+ module InheritedResources
2
+ # = Dumb Responder
3
+ #
4
+ # This responder discards all messages sent to him.
5
+ #
6
+ class DumbResponder
7
+
8
+ instance_methods.each do |m|
9
+ undef_method m unless m =~ /^__/
10
+ end
11
+
12
+ # This is like a good husband, he will just listen everything that his wife
13
+ # says (which is a lot) without complaining. :)
14
+ #
15
+ def method_missing(*args)
16
+ nil
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,65 @@
1
+ module InheritedResources
2
+
3
+ # = has_scopes
4
+ #
5
+ # This module in included in your controller when has_scope is called for the
6
+ # first time.
7
+ #
8
+ module HasScopeHelpers
9
+ TRUE_VALUES = ["true", true, "1", 1] unless self.const_defined?(:TRUE_VALUES)
10
+
11
+ protected
12
+
13
+ # Overwrites apply to scope to implement default scope logic.
14
+ #
15
+ def apply_scope_to(target_object) #:nodoc:
16
+ @current_scopes ||= {}
17
+
18
+ self.scopes_configuration.each do |scope, options|
19
+ next unless apply_scope_to_action?(options)
20
+ key = options[:as]
21
+
22
+ if params.key?(key)
23
+ value, call_scope = params[key], true
24
+ elsif options.key?(:default)
25
+ value, call_scope = options[:default], true
26
+ value = value.call(self) if value.is_a?(Proc)
27
+ end
28
+
29
+ if call_scope
30
+ @current_scopes[key] = value
31
+
32
+ if options[:boolean]
33
+ target_object = target_object.send(scope) if TRUE_VALUES.include?(value)
34
+ else
35
+ target_object = target_object.send(scope, value)
36
+ end
37
+ end
38
+ end
39
+
40
+ target_object
41
+ end
42
+
43
+ # Given an options with :only and :except arrays, check if the scope
44
+ # can be performed in the current action.
45
+ #
46
+ def apply_scope_to_action?(options) #:nodoc:
47
+ if options[:only].empty?
48
+ if options[:except].empty?
49
+ true
50
+ else
51
+ !options[:except].include?(action_name.to_sym)
52
+ end
53
+ else
54
+ options[:only].include?(action_name.to_sym)
55
+ end
56
+ end
57
+
58
+ # Returns the scopes used in this action.
59
+ #
60
+ def current_scopes
61
+ @current_scopes || {}
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,151 @@
1
+ module ActionController #:nodoc:
2
+ class Base #:nodoc:
3
+ attr_accessor :formats
4
+
5
+ # Defines mimes that are rendered by default when invoking respond_with.
6
+ #
7
+ # Examples:
8
+ #
9
+ # respond_to :html, :xml, :json
10
+ #
11
+ # All actions on your controller will respond to :html, :xml and :json.
12
+ #
13
+ # But if you want to specify it based on your actions, you can use only and
14
+ # except:
15
+ #
16
+ # respond_to :html
17
+ # respond_to :xml, :json, :except => [ :edit ]
18
+ #
19
+ # The definition above explicits that all actions respond to :html. And all
20
+ # actions except :edit respond to :xml and :json.
21
+ #
22
+ # You can specify also only parameters:
23
+ #
24
+ # respond_to :rjs, :only => :create
25
+ #
26
+ def self.respond_to(*mimes)
27
+ options = mimes.extract_options!
28
+
29
+ only_actions = Array(options.delete(:only))
30
+ except_actions = Array(options.delete(:except))
31
+
32
+ mimes.each do |mime|
33
+ mime = mime.to_sym
34
+ mimes_for_respond_to[mime] = {}
35
+ mimes_for_respond_to[mime][:only] = only_actions unless only_actions.empty?
36
+ mimes_for_respond_to[mime][:except] = except_actions unless except_actions.empty?
37
+ end
38
+ end
39
+
40
+ # Clear all mimes in respond_to.
41
+ #
42
+ def self.clear_respond_to
43
+ write_inheritable_attribute(:mimes_for_respond_to, ActiveSupport::OrderedHash.new)
44
+ end
45
+
46
+ class_inheritable_reader :mimes_for_respond_to
47
+ clear_respond_to
48
+
49
+ # If ApplicationController is already defined around here, we have to set
50
+ # mimes_for_respond_to hash as well.
51
+ #
52
+ ApplicationController.clear_respond_to if defined?(ApplicationController)
53
+
54
+ def respond_to(*mimes, &block)
55
+ raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?
56
+
57
+ responder = ActionController::MimeResponds::Responder.new(self)
58
+ mimes = collect_mimes_from_class_level if mimes.empty?
59
+ mimes.each { |mime| responder.send(mime) }
60
+ block.call(responder) if block_given?
61
+
62
+ if format = responder.negotiate_mime
63
+ self.response.template.template_format = format.to_sym
64
+ self.response.content_type = format.to_s
65
+ self.formats = [ format.to_sym ]
66
+
67
+ if response = responder.response_for(format)
68
+ response.call
69
+ else
70
+ default_render
71
+ end
72
+ else
73
+ head :not_acceptable
74
+ end
75
+ end
76
+
77
+ def respond_with(*resources, &block)
78
+ respond_to(&block)
79
+ rescue ActionView::MissingTemplate
80
+ options = resources.extract_options!
81
+ (options.delete(:responder) || responder).call(self, resources, options)
82
+ end
83
+
84
+ def responder
85
+ ActionController::Responder
86
+ end
87
+
88
+ protected
89
+
90
+ # Collect mimes declared in the class method respond_to valid for the
91
+ # current action.
92
+ #
93
+ def collect_mimes_from_class_level #:nodoc:
94
+ action = action_name.to_sym
95
+
96
+ mimes_for_respond_to.keys.select do |mime|
97
+ config = mimes_for_respond_to[mime]
98
+
99
+ if config[:except]
100
+ !config[:except].include?(action)
101
+ elsif config[:only]
102
+ config[:only].include?(action)
103
+ else
104
+ true
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ module MimeResponds
111
+ class Responder #:nodoc:
112
+ attr_reader :order
113
+
114
+ def any(*args, &block)
115
+ if args.any?
116
+ args.each { |type| send(type, &block) }
117
+ else
118
+ custom(Mime::ALL, &block)
119
+ end
120
+ end
121
+ alias :all :any
122
+
123
+ def custom(mime_type, &block)
124
+ mime_type = mime_type.is_a?(Mime::Type) ? mime_type : Mime::Type.lookup(mime_type.to_s)
125
+ @order << mime_type
126
+ @responses[mime_type] ||= block
127
+ end
128
+
129
+ def response_for(mime)
130
+ @responses[mime] || @responses[Mime::ALL]
131
+ end
132
+
133
+ def negotiate_mime
134
+ @mime_type_priority.each do |priority|
135
+ if priority == Mime::ALL
136
+ return @order.first
137
+ elsif @order.include?(priority)
138
+ return priority
139
+ end
140
+ end
141
+
142
+ if @order.include?(Mime::ALL)
143
+ return Mime::SET.first if @mime_type_priority.first == Mime::ALL
144
+ return @mime_type_priority.first
145
+ end
146
+
147
+ nil
148
+ end
149
+ end
150
+ end
151
+ end