extended_inherited_resources 0.1.0

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